[object Object]

← back to Commercialrealestate

CRCP crexi: normalize v2 'sales-N' ids to bare N + pass zip through (both extractors); DB scraper on new /search/ scheme (TK-12033)

9b71f50cca22a9e8600425837b7ebfa0e224de02 · 2026-09-23 11:55:55 -0700 · Steve Abrams

Crexi's universal-search/v2 ids arrive as 'sales-2066415' but the canonical detail URL is
/properties/2066415/<slug> and our historical ids are crxN. Passing the prefix through (b2acc84)
stored source links that 302 to a dead brokerage-search page, forked the id namespace, and made
crcp-scope.js isCalifornia() fail closed so every new row was hidden on CA city subdomains.
scrape-crexi-loopnet.js: the old ?types[]=&placeIds[]= URL no longer navigates (read as a false
THROTTLED); use the verified /search/<slug>-properties-for-sale/los-angeles-ca scheme.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7HcZ26MePoJ5QpcvDhKmH

Files touched

Diff

commit 9b71f50cca22a9e8600425837b7ebfa0e224de02
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 11:55:55 2026 -0700

    CRCP crexi: normalize v2 'sales-N' ids to bare N + pass zip through (both extractors); DB scraper on new /search/ scheme (TK-12033)
    
    Crexi's universal-search/v2 ids arrive as 'sales-2066415' but the canonical detail URL is
    /properties/2066415/<slug> and our historical ids are crxN. Passing the prefix through (b2acc84)
    stored source links that 302 to a dead brokerage-search page, forked the id namespace, and made
    crcp-scope.js isCalifornia() fail closed so every new row was hidden on CA city subdomains.
    scrape-crexi-loopnet.js: the old ?types[]=&placeIds[]= URL no longer navigates (read as a false
    THROTTLED); use the verified /search/<slug>-properties-for-sale/los-angeles-ca scheme.
    
    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01X7HcZ26MePoJ5QpcvDhKmH
---
 scripts/refresh-listings-multi.js |  7 ++++---
 scripts/scrape-crexi-loopnet.js   |  5 ++++-
 scripts/sources/firms.js          | 10 +++++++---
 3 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/scripts/refresh-listings-multi.js b/scripts/refresh-listings-multi.js
index 79d6a4e..1403932 100644
--- a/scripts/refresh-listings-multi.js
+++ b/scripts/refresh-listings-multi.js
@@ -61,13 +61,14 @@ async function makeBrowser() {
             : (a.address && typeof a.address === 'object' ? a.address : ((a.locations && a.locations[0]) || {}));
           const st = String(loc.stateCode || loc.state || loc.stateName || '').toUpperCase();
           if (st && st !== 'CA' && st !== 'CALIFORNIA') continue;
-          const id = a.id || a.assetId;
+          const rawId = a.id || a.assetId;   // "sales-N" -> "N" (canonical detail URL + crxN id namespace, TK-12033)
+          const id = rawId == null ? null : String(rawId).replace(/^(?:sales|lease|auction)-/i, "");
           const price = (a.propertyPrice && a.propertyPrice.total) || a.askingPrice || a.price;
           const city = loc.city || loc.cityName;
           const address = loc.streetAddress || loc.address || loc.fullAddress || a.propertyName || a.name || '';
           const attrs = a.propertyAttributes || {};
           if (id && price && city && !assets.has(id)) assets.set(id, {
-            id, price: +price, city, address,
+            id, price: +price, city, address, zip: String(loc.zip || ""),
             cap: typeof a.capRate === 'number' ? a.capRate : null, urlSlug: a.urlSlug || '',
             desc: String(a.description || ''),
             types: (Array.isArray(a.types) && a.types.length) ? a.types : [attrs.type, attrs.subType].filter(Boolean),
@@ -111,7 +112,7 @@ async function makeBrowser() {
     const type = (a.types || []).some(t => /retail/i.test(t)) ? 'Retail' : (a.types || []).some(t => /office/i.test(t)) ? 'Office' : (a.types || []).some(t => /mixed/i.test(t)) ? 'Mixed-use' : 'Multifamily';
     const units = (() => { const m = a.desc.match(/(?:\|\s*)?\b(\d{1,3})\s*units?\b/i); if (m) { const n = +m[1]; if (n >= 1 && n <= 500 && a.price / n >= 50000) return n; } return null; })();
     data.listings.push({
-      id: 'crx' + a.id, address: a.address, city: a.city, metro: a.metro, zip: '', type,
+      id: 'crx' + a.id, address: a.address, city: a.city, metro: a.metro, zip: a.zip || '', type,
       price: a.price, units: units || 1, sqft: null, cap_rate: a.cap, verified: false,
       year_built: null, rent_control: 'verify', status: 'Active',
       upside_note: `Auto-added by multi-metro Crexi sweep ${today} (${a.metro}). ${a.cap ? 'Broker cap ' + a.cap + '% (unverified).' : 'Cap not disclosed.'} Verify before acting.`,
diff --git a/scripts/scrape-crexi-loopnet.js b/scripts/scrape-crexi-loopnet.js
index bb0f1d2..bcc74b6 100644
--- a/scripts/scrape-crexi-loopnet.js
+++ b/scripts/scrape-crexi-loopnet.js
@@ -41,7 +41,10 @@ const ALL_TYPES = ['Multifamily', 'Retail', 'Office', 'Industrial', 'Mixed Use']
 const TYPES = TYPE_ARG.toLowerCase() === 'all' ? ALL_TYPES : [TYPE_ARG];
 // LA County placeId (recon-verified to return LA results with street address + price)
 const LA_PLACE = 'ChIJE9on3F3HwoAR9AhGJW_fL-I';
-const searchUrl = t => `https://www.crexi.com/properties?types%5B%5D=${encodeURIComponent(t)}&placeIds%5B%5D=${LA_PLACE}`;
+// Crexi moved to /search/<type-slug>-properties-for-sale/<market> (2026-09); the old ?types[]=&placeIds[]= URL no longer
+// navigates (it read as a false "THROTTLED" here). Slugs verified live 2026-09-23, each firing universal-search/v2 (TK-12033).
+const TYPE_SLUG = { Multifamily: "multifamily", Retail: "retail", Office: "office", Industrial: "industrial", "Mixed Use": "mixed-use" };
+const searchUrl = t => `https://www.crexi.com/search/${TYPE_SLUG[t] || encodeURIComponent(String(t).toLowerCase().replace(/\s+/g, "-"))}-properties-for-sale/los-angeles-ca`;
 
 // ── resume cache: seen detail ids (crawled) + written addr keys (ingested) ──────────────────────
 const PROG = path.join(__dirname, '..', 'data', 'crexi-progress.json');
diff --git a/scripts/sources/firms.js b/scripts/sources/firms.js
index 23f2973..86ba78f 100644
--- a/scripts/sources/firms.js
+++ b/scripts/sources/firms.js
@@ -128,14 +128,18 @@ function crexiExtract(json) {
       : (a.address && typeof a.address === 'object' ? a.address : ((a.locations && a.locations[0]) || {}));
     const st = String(loc.stateCode || loc.state || loc.stateName || '').toUpperCase();
     if (st && st !== 'CA' && st !== 'CALIFORNIA') return null;   // California-only gate (contamination guard)
-    const id = a.id || a.assetId;
+    // v2 ids arrive as "sales-2066415" / "lease-…" but the canonical detail URL (and our historical crxN ids)
+    // use the bare number — /properties/2066415/<urlSlug>. Keeping the prefix 302s to a dead "brokerage search"
+    // page, forks the id namespace, and fails crcp-scope.js isCalifornia() closed (TK-12033).
+    const rawId = a.id || a.assetId;
+    const id = rawId == null ? null : String(rawId).replace(/^(?:sales|lease|auction)-/i, "");
     const price = (a.propertyPrice && a.propertyPrice.total) || a.askingPrice || a.price;
     const city = loc.city || loc.cityName;
     const address = loc.streetAddress || loc.address || loc.fullAddress || a.propertyName || a.name || '';
     if (!id || !price || !city) return null;
     const attrs = a.propertyAttributes || {};
     return {
-      id, price: +price, city, address,
+      id, price: +price, city, address, zip: String(loc.zip || ""),
       cap: typeof a.capRate === 'number' ? a.capRate : null, urlSlug: a.urlSlug || '',
       desc: String(a.description || ''),
       types: (Array.isArray(a.types) && a.types.length) ? a.types : [attrs.type, attrs.subType].filter(Boolean)
@@ -280,7 +284,7 @@ function toListing(a, firm) {
   const src = firm.sourceUrl ? firm.sourceUrl(a)
     : (a.urlSlug && /^https?:/.test(a.urlSlug) ? a.urlSlug : '');
   return {
-    id: firm.idPrefix + a.id, address: a.address, city: a.city, zip: '', type,
+    id: firm.idPrefix + a.id, address: a.address, city: a.city, zip: a.zip || '', type,
     price: a.price, units: units || 1, sqft: null, cap_rate: a.cap ?? null, verified: false,
     year_built: null, rent_control: 'verify', status: 'Active',
     firm: firm.name, firm_key: firm.key,

← 94a236d auto-data-snapshot: 2026-09-23T10:03:20 (3 data files) — dat  ·  back to Commercialrealestate  ·  CRCP data: repair 47 crexi rows minted with v2 'sales-' id p 6c44ca0 →