[object Object]

← back to Commercialrealestate

CRCP: broker-direct SUBSTITUTES the aggregator row (sub out Crexi when the firm's own listing is found) — Steve 2026-08-18

6074d06ac2cfa7d7af3a9476c31ae7145ab2e1cd · 2026-08-18 20:50:48 -0700 · Steve Abrams

- mergedRanked(): canonical street-address key (Blvd=Boulevard etc) so a broker-direct listing matches its Crexi twin; on match SWAP source -> firm's own page + re-attribute + fill gaps instead of skipping. 41 subbed on first pass.
- _brokerDirectSubbed counter; total tightened (no same-property double rows)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 6074d06ac2cfa7d7af3a9476c31ae7145ab2e1cd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 20:50:48 2026 -0700

    CRCP: broker-direct SUBSTITUTES the aggregator row (sub out Crexi when the firm's own listing is found) — Steve 2026-08-18
    
    - mergedRanked(): canonical street-address key (Blvd=Boulevard etc) so a broker-direct listing matches its Crexi twin; on match SWAP source -> firm's own page + re-attribute + fill gaps instead of skipping. 41 subbed on first pass.
    - _brokerDirectSubbed counter; total tightened (no same-property double rows)
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/serve.js | 48 +++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 37 insertions(+), 11 deletions(-)

diff --git a/scripts/serve.js b/scripts/serve.js
index e49aae0..39da358 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -1990,20 +1990,46 @@ function mergedRanked() {
   if (_rankedMerge.key === key && _rankedMerge.json) return _rankedMerge.json;
   const base = JSON.parse(fs.readFileSync(rankedPath, 'utf8'));
   const ranked = Array.isArray(base.ranked) ? base.ranked : [];
-  const addrKey = r => String(r.address || '').toLowerCase().trim() + '|' + String(r.zip || '').trim();
-  const have = new Set(ranked.map(addrKey));
-  let added = 0;
+  // Canonical street-address key so a broker-direct "1351 Westwood Blvd" matches a Crexi
+  // "1351 Westwood Boulevard" (suffix words normalized, then non-alphanumerics stripped).
+  const SUF = { boulevard: 'blvd', avenue: 'ave', street: 'st', drive: 'dr', road: 'rd', place: 'pl',
+    court: 'ct', lane: 'ln', terrace: 'ter', parkway: 'pkwy', highway: 'hwy', square: 'sq', circle: 'cir',
+    north: 'n', south: 's', east: 'e', west: 'w' };
+  const addrKey = r => String(r.address || '').toLowerCase()
+    .replace(/\b(boulevard|avenue|street|drive|road|place|court|lane|terrace|parkway|highway|square|circle|north|south|east|west)\b/g, m => SUF[m])
+    .replace(/[^a-z0-9]/g, '');
+  const idxByAddr = new Map();
+  ranked.forEach((r, i) => { const k = addrKey(r); if (k && !idxByAddr.has(k)) idxByAddr.set(k, i); });
+
+  // Steve 2026-08-18: "sub out crexi if found in data" — a broker-DIRECT listing (the firm's own site,
+  // real permalink + attribution, higher trust) SUBSTITUTES the aggregator row for the same property
+  // instead of being skipped. We keep the existing row's richer computed/structured fields but swap its
+  // source → the broker's own listing page, re-attribute the firm, and fill any gaps from the direct row.
+  let added = 0, substituted = 0;
   for (const f of BROKER_FEEDS) {
-    try {
-      const feed = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', f), 'utf8'));
-      for (const row of (feed.listings || [])) {
-        const k = addrKey(row);
-        if (have.has(k)) continue;                 // a crexi row already covers this address
-        have.add(k); ranked.push(row); added++;
+    let feed; try { feed = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', f), 'utf8')); } catch (_) { continue; }
+    for (const row of (feed.listings || [])) {
+      const k = addrKey(row); if (!k) continue;
+      if (idxByAddr.has(k)) {
+        const i = idxByAddr.get(k); const ex = ranked[i];
+        if (ex._sourced) continue;                 // already a broker-direct row → don't double-sub
+        ranked[i] = { ...ex,
+          source: row.source, source_host: row.source_host, source_of_truth: 'broker-direct',
+          broker_firm: row.broker_firm || ex.broker_firm, firm: row.firm || ex.firm,
+          _sourced: row._sourced, _subbedFrom: ex.source_host || 'aggregator',
+          // fill gaps from the direct listing where the aggregator row was null
+          price: ex.price != null ? ex.price : row.price,
+          cap_rate: ex.cap_rate != null ? ex.cap_rate : row.cap_rate,
+          units: ex.units != null ? ex.units : row.units,
+          type: ex.type || row.type,
+        };
+        substituted++;
+      } else {
+        idxByAddr.set(k, ranked.length); ranked.push(row); added++;
       }
-    } catch (_) { /* a missing/!valid feed just contributes nothing */ }
+    }
   }
-  const out = { ...base, ranked, _brokerDirectAdded: added };
+  const out = { ...base, ranked, _brokerDirectAdded: added, _brokerDirectSubbed: substituted };
   _rankedMerge = { key, json: out };
   return out;
 }

← 4eac4b6 CRCP gov-agents.html: add grid/list/table view system (was t  ·  back to Commercialrealestate  ·  crcp-theme: theme fleet custom SURFACE vars (--panel/--panel d5a0b83 →