← back to Commercialrealestate
Fix Crexi extractor for universal-search/v2 API (json.items + new schema + CA-state gate); unblocks stale feed
b2acc84503940cdf51d7a5ef7a6aff6936d42f11 · 2026-09-22 15:10:57 -0700 · Steve Abrams
Files touched
M scripts/refresh-listings-multi.jsM scripts/sources/firms.js
Diff
commit b2acc84503940cdf51d7a5ef7a6aff6936d42f11
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 22 15:10:57 2026 -0700
Fix Crexi extractor for universal-search/v2 API (json.items + new schema + CA-state gate); unblocks stale feed
---
scripts/refresh-listings-multi.js | 24 +++++++++++++++++++-----
scripts/sources/firms.js | 28 ++++++++++++++++++++++------
2 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/scripts/refresh-listings-multi.js b/scripts/refresh-listings-multi.js
index 82ea98a..79d6a4e 100644
--- a/scripts/refresh-listings-multi.js
+++ b/scripts/refresh-listings-multi.js
@@ -51,13 +51,27 @@ async function makeBrowser() {
if (!resp.url().includes('api.crexi.com')) return;
try {
const j = await resp.json();
- const arr = Array.isArray(j) ? j : (j.data || j.assets || j.results || []);
+ // Crexi migrated (2026-09) to universal-search/v2: rows in j.items, address is an ARRAY carrying
+ // stateCode, price under propertyPrice.total, type/subType under propertyAttributes. Old-shape
+ // fallbacks kept. Drop rows whose state is present and not CA (cross-state contamination guard).
+ const arr = Array.isArray(j) ? j : (j.items || j.data || j.assets || j.results || []);
if (Array.isArray(arr)) for (const a of arr) {
- const id = a.id || a.assetId, price = a.askingPrice || a.price, loc = (a.locations && a.locations[0]) || {};
- if (id && price && loc.city && !assets.has(id)) assets.set(id, {
- id, price: +price, city: loc.city, address: loc.address || (a.name || ''),
+ if (!a || typeof a !== 'object') continue;
+ const loc = Array.isArray(a.address) ? (a.address[0] || {})
+ : (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 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,
cap: typeof a.capRate === 'number' ? a.capRate : null, urlSlug: a.urlSlug || '',
- desc: String(a.description || ''), types: a.types || [], metro: null,
+ desc: String(a.description || ''),
+ types: (Array.isArray(a.types) && a.types.length) ? a.types : [attrs.type, attrs.subType].filter(Boolean),
+ metro: null,
});
}
} catch (_) {}
diff --git a/scripts/sources/firms.js b/scripts/sources/firms.js
index 75cd98b..23f2973 100644
--- a/scripts/sources/firms.js
+++ b/scripts/sources/firms.js
@@ -113,16 +113,32 @@ function genericExtract(json) {
// Crexi's verified extractor — its API matches genericExtract anyway, but we keep it explicit so the
// reference source is never silently changed by a heuristic tweak.
function crexiExtract(json) {
- const arr = Array.isArray(json) ? json : (json.data || json.assets || json.results || []);
+ // Crexi migrated (2026-09) to api.crexi.com/universal-search/v2/search: rows now live in json.items
+ // and use a new schema — `address` is an ARRAY of {stateCode,city,streetAddress,zip,...}, price is
+ // `propertyPrice.total`, and type/subType live under `propertyAttributes`. Old-shape fallbacks
+ // (data/assets/results + locations[]/askingPrice/capRate) are kept so this survives either schema.
+ // The search API also returns NATIONAL rows on the initial (unscoped) call, so we drop any row whose
+ // state is present and not California — the exact cross-state contamination class the
+ // crcp-contamination-canary guards (e.g. Pasadena/Georgetown TX bleeding onto CA city subdomains).
+ const arr = Array.isArray(json) ? json : (json.items || json.data || json.assets || json.results || []);
if (!Array.isArray(arr)) return [];
return arr.map(a => {
- const loc = (a.locations && a.locations[0]) || {};
- const price = a.askingPrice || a.price;
- if (!(a.id || a.assetId) || !price || !loc.city) return null;
+ if (!a || typeof a !== 'object') return null;
+ const loc = Array.isArray(a.address) ? (a.address[0] || {})
+ : (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;
+ 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: a.id || a.assetId, price: +price, city: loc.city, address: loc.address || (a.name || ''),
+ id, price: +price, city, address,
cap: typeof a.capRate === 'number' ? a.capRate : null, urlSlug: a.urlSlug || '',
- desc: String(a.description || ''), types: a.types || []
+ desc: String(a.description || ''),
+ types: (Array.isArray(a.types) && a.types.length) ? a.types : [attrs.type, attrs.subType].filter(Boolean)
};
}).filter(Boolean);
}
← 9a4acb8 afternoon CRE update 2026-09-22
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-09-22T15:36:13 (3 data files) — dat 52b68ab →