[object Object]

← back to Nationalrealestate

usre: Tier-1 accurate firm-phone backfill from cre office lines (TK-10687)

88312fc72d0f969510149ecc0639ad04e5c41437 · 2026-08-18 13:47:11 -0700 · Steve Abrams

Promotes the SHARED office switchboard (a canonical phone 2+ brokers at a firm use,
per the vetted cre.broker/Crexi data) into usre.firm — no web, instant, high accuracy.
Guards: canonicalize phones before counting (format variants fragmented the share
count), skip toll-free + mega-brokerages (their corporate line must come from their
own site, not one agent's record). 16 verified LA office lines matched (Major
Properties 8-broker line, Lee & Associates 5, Oppenheim Group 3). COALESCE-only,
provenance 'cre-backfill'. $0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 88312fc72d0f969510149ecc0639ad04e5c41437
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 13:47:11 2026 -0700

    usre: Tier-1 accurate firm-phone backfill from cre office lines (TK-10687)
    
    Promotes the SHARED office switchboard (a canonical phone 2+ brokers at a firm use,
    per the vetted cre.broker/Crexi data) into usre.firm — no web, instant, high accuracy.
    Guards: canonicalize phones before counting (format variants fragmented the share
    count), skip toll-free + mega-brokerages (their corporate line must come from their
    own site, not one agent's record). 16 verified LA office lines matched (Major
    Properties 8-broker line, Lee & Associates 5, Oppenheim Group 3). COALESCE-only,
    provenance 'cre-backfill'. $0.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/backfill-firm-phones-from-cre.mjs | 82 +++++++++++++++++++++++++++++++
 1 file changed, 82 insertions(+)

diff --git a/scripts/backfill-firm-phones-from-cre.mjs b/scripts/backfill-firm-phones-from-cre.mjs
new file mode 100644
index 0000000..23324e4
--- /dev/null
+++ b/scripts/backfill-firm-phones-from-cre.mjs
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+// backfill-firm-phones-from-cre.mjs — TK-10687 Tier-1 (accurate, instant, $0, no web).
+//
+// The commercialrealestate `cre` DB already holds vetted broker contact data (from Crexi/Redfin).
+// The strongest FIRM-level signal there: a phone SHARED by 2+ brokers at the same firm = the office
+// switchboard (not a personal cell). We aggregate that office line per firm and promote it into the
+// usre.firm registry (COALESCE-only, provenance 'cre-backfill'), so commercial firms — exactly the
+// ones RENTV / CRCP care about — get an accurate main line without any web scraping.
+//
+// Usage: node scripts/backfill-firm-phones-from-cre.mjs            (dry-run)
+//        node scripts/backfill-firm-phones-from-cre.mjs --apply    (write to usre.firm)
+'use strict';
+import pg from 'pg';
+
+const USRE = process.env.DATABASE_URL || 'postgresql:///usre?host=/tmp';
+const CRE = process.env.CRE_DATABASE_URL || 'postgresql:///cre?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+const _mi = process.argv.indexOf('--min-share');
+const MIN_SHARE = _mi > -1 ? Number(process.argv[_mi + 1]) : 2;   // ≥N brokers on the number = office line
+// Mega-brokerages: their corporate line must come from their OWN site (web tier), never from one
+// agent's cre record — else thousands of their brokers would show a single agent's number.
+const MEGA = /(exp realty|compass|coldwell|keller williams|re\/?max|berkshire|century 21|sotheby|douglas elliman|redfin|opendoor|weichert|realty one|ehome)/i;
+
+const canon = p => { const d = String(p || '').replace(/\D/g, '').replace(/^1(?=\d{10}$)/, ''); return d.length === 10 ? `(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}` : null; };
+const norm = s => String(s || '').toLowerCase().replace(/\b(inc|llc|corp|corporation|co|company|the|and|&|ltd|lp|group)\b/g, ' ').replace(/[^a-z0-9]+/g, ' ').trim();
+const TOLLFREE = /^\(?(?:800|833|844|855|866|877|888)/;
+
+async function main() {
+  const cre = new pg.Pool({ connectionString: CRE });
+  const usre = new pg.Pool({ connectionString: USRE });
+
+  // Raw (firm, broker phone) pairs — one row per broker so we can count SHARED lines after
+  // normalizing away format variants ("(310) 529-2052" vs "310-529-2052").
+  const rows = (await cre.query(`
+    SELECT f.name AS firm, b.phone
+      FROM broker b JOIN firm f ON f.id=b.firm_id
+     WHERE coalesce(nullif(b.phone,''),'')<>'' AND b.is_test=false`)).rows;
+
+  // count distinct brokers per (normalized firm, canonical phone); the office line = the shared one.
+  const counts = new Map();   // key `${normFirm}|${canonPhone}` → {firm, phone, n}
+  for (const r of rows) {
+    if (MEGA.test(r.firm)) continue;                    // mega-brokerage corporate line ≠ one agent's record
+    const p = canon(r.phone); if (!p || TOLLFREE.test(p)) continue;
+    const nf = norm(r.firm); if (!nf) continue;
+    const k = nf + '|' + p; const c = counts.get(k);
+    if (c) c.n++; else counts.set(k, { firm: r.firm, nf, phone: p, n: 1 });
+  }
+  // per firm, keep the most-shared line that clears MIN_SHARE
+  const byFirm = new Map();
+  for (const v of counts.values()) {
+    if (v.n < MIN_SHARE) continue;
+    const cur = byFirm.get(v.nf);
+    if (!cur || v.n > cur.n) byFirm.set(v.nf, v);
+  }
+  console.log(`cre office-line candidates (≥${MIN_SHARE} brokers share the canonical number, non-toll-free, non-mega): ${byFirm.size} firms`);
+
+  // match to usre.firm (CA, missing phone) by normalized name
+  const targets = (await usre.query(`SELECT id, name FROM firm WHERE license_state='CA' AND coalesce(nullif(phone,''),'')=''`)).rows;
+  const idx = new Map(); for (const t of targets) idx.set(norm(t.name), t);
+
+  let matched = 0;
+  const client = APPLY ? await usre.connect() : null;
+  for (const [k, v] of byFirm) {
+    const t = idx.get(k); if (!t) continue;
+    matched++;
+    console.log(`  ${t.name} → 📞 ${v.phone}  (${v.n} brokers share it)`);
+    if (APPLY) {
+      await client.query(
+        `UPDATE firm SET phone=COALESCE(NULLIF(phone,''),$2),
+           phone_source=CASE WHEN COALESCE(NULLIF(phone,''),'')='' THEN 'cre-backfill' ELSE phone_source END,
+           contact_attempts=contact_attempts+1, contact_enriched_at=now(),
+           phone_status=CASE WHEN COALESCE(NULLIF(phone,''),'')='' THEN 'found' ELSE phone_status END
+         WHERE id=$1`, [t.id, v.phone]);
+      await client.query(`INSERT INTO firm_contacts (firm_id,kind,value,source_url) VALUES ($1,'phone',$2,'cre-backfill')
+                            ON CONFLICT (firm_id,kind,value) DO NOTHING`, [t.id, v.phone]);
+    }
+  }
+  if (client) client.release();
+  console.log(`\nDONE: ${matched} usre CA firms matched an accurate cre office line · ${APPLY ? 'WRITTEN' : 'dry-run'} · $0`);
+  await cre.end(); await usre.end();
+}
+main().catch(e => { console.error('FATAL', e); process.exit(1); });

← e72841d usre: harden firm-phone enrichment for accuracy (name-match  ·  back to Nationalrealestate  ·  usre: CA-area-code guard on firm-phone enrichment (TK-10687) 4855043 →