← 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
M scripts/refresh-listings-multi.jsM scripts/scrape-crexi-loopnet.jsM scripts/sources/firms.js
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 →