[object Object]

← back to Commercialrealestate

backfill: garbage-address filter + unique-domain-only attribution (skip shared firm sites); applied 24 clean direct listings to 5 agents (reversible source=broker-site)

48a8e858605b84a1925f6ce6ca1b10e276de5923 · 2026-08-19 09:22:14 -0700 · Steve Abrams

Files touched

Diff

commit 48a8e858605b84a1925f6ce6ca1b10e276de5923
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 09:22:14 2026 -0700

    backfill: garbage-address filter + unique-domain-only attribution (skip shared firm sites); applied 24 clean direct listings to 5 agents (reversible source=broker-site)
---
 scripts/backfill-broker-listings.js | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/scripts/backfill-broker-listings.js b/scripts/backfill-broker-listings.js
index 3e31b94..2d32600 100644
--- a/scripts/backfill-broker-listings.js
+++ b/scripts/backfill-broker-listings.js
@@ -18,7 +18,14 @@ const APPLY = process.argv.includes('--apply');
 const BIG = /cbre|kw\.com|yourkwoffice|kellerwilliams|marcusmillichap|kidder|coldwell|compass|remax|century21|colliers|jll|cushman|berkshire/i;
 
 const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36';
+const _domCache = new Map(); // fetch each domain ONCE (many agents share a firm site — don't hammer)
+const domainOf = u => { try { return new URL(/^https?:/i.test(u) ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return u; } };
 async function fetchSite(url) {
+  const dom = domainOf(url);
+  if (_domCache.has(dom)) return _domCache.get(dom);
+  const r = await _fetchSite(url); _domCache.set(dom, r); return r;
+}
+async function _fetchSite(url) {
   try {
     if (!/^https?:\/\//i.test(url)) url = 'https://' + url.replace(/^\/+/, ''); // scheme-less sites (www.x.com)
     const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 15000);
@@ -41,7 +48,11 @@ function extractListings(html) {
     const window = text.slice(Math.max(0, i - 120), i + 180);
     const pm = window.match(/\$\s?([0-9]{3,}(?:,[0-9]{3})+)/);
     const price = pm ? +pm[1].replace(/,/g, '') : null;
-    if (price && price >= 100000 && price <= 500000000) out.push({ address: a.trim(), price });
+    const addr = a.trim();
+    // reject garbage: leading-zero / placeholder house numbers ("000 Street ..."), too-short
+    const houseNo = (addr.match(/^\d+/) || ['0'])[0];
+    const bad = /^0+$/.test(houseNo) || /^0+\s/.test(addr) || /\b000\b/.test(addr) || addr.length < 8;
+    if (price && price >= 100000 && price <= 500000000 && !bad) out.push({ address: addr, price });
   }
   // dedupe by address
   const seen = new Set(); return out.filter(l => !seen.has(l.address.toLowerCase()) && seen.add(l.address.toLowerCase()));
@@ -51,6 +62,12 @@ function extractListings(html) {
   const brokers = (await db.pool.query(
     `SELECT id, name, website FROM broker WHERE website IS NOT NULL AND website !~* 'crexi' AND website !~* $1 LIMIT $2`,
     [BIG.source, LIMIT])).rows;
+  // Which domains are shared by >1 broker? Their listings are FIRM-level, not one agent's —
+  // don't attribute those to a single agent (the misattribution trap). Only unique domains get linked.
+  const shareRows = (await db.pool.query(
+    `SELECT website FROM broker WHERE website IS NOT NULL AND website !~* 'crexi' AND website !~* $1`, [BIG.source])).rows;
+  const domCount = {}; for (const r of shareRows) { const d = domainOf(r.website); domCount[d] = (domCount[d] || 0) + 1; }
+  const isShared = url => (domCount[domainOf(url)] || 0) > 1;
   console.log(`\n== Backfill pilot: ${brokers.length} boutique broker sites · ${APPLY ? 'APPLY (writing)' : 'DRY-RUN'} ==\n`);
   let totalFound = 0, wrote = 0, blocked = 0;
   for (const b of brokers) {
@@ -58,9 +75,10 @@ function extractListings(html) {
     if (!r.ok) { console.log(`  ✗ ${b.name} — ${b.website} [${r.status}${r.err ? ' ' + r.err : ''}] (Phase-2 openclaw)`); blocked++; continue; }
     const listings = extractListings(r.html);
     totalFound += listings.length;
-    console.log(`  ${listings.length ? '✓' : '·'} ${b.name} — ${b.website} → ${listings.length} listing(s)`);
+    const shared = isShared(b.website);
+    console.log(`  ${listings.length ? '✓' : '·'} ${b.name} — ${b.website} → ${listings.length} listing(s)${shared && listings.length ? ' [SHARED firm site — not attributed]' : ''}`);
     listings.slice(0, 4).forEach(l => console.log(`       $${l.price.toLocaleString()}  ${l.address}`));
-    if (APPLY && listings.length) {
+    if (APPLY && listings.length && !shared) {  // only attribute UNIQUE-domain (personal-site) listings to the agent
       for (const l of listings) {
         // insert listing (tagged reversible) + link to broker; dedup by address+broker
         const ins = await db.pool.query(

← b422f11 CRCP: every agent opens its own page — link agent names in t  ·  back to Commercialrealestate  ·  CRCP: backfill real broker phones for Showcase listings — FR 7361844 →