← back to Commercialrealestate

scripts/backfill-crexi-firms.js

65 lines

#!/usr/bin/env node
// backfill-crexi-firms.js — ALL-DAY, parallel, FREE ($0) firm-phone backfill for the Crexi-base rows.
// The ~2,400 usre-miss rows cluster into ~1,248 firms (branch-specific: "Marcus & Millichap - Encino"
// etc.), and a firm's local office line is a legitimate contact. This resolves each firm's phone via a
// Google search in openclaw's REAL Chrome (clears anti-bot, logged-in, $0) — running N tabs IN PARALLEL
// (Steve: "max-it all day... parallel in browsers if needed... just go"). Appends to the shared
// data/broker-phone-overlay.json under 'firm:<normfirm>'; serve.js overlays it onto every row at that
// firm lacking a phone. Resumable (skips firms already resolved or marked no-hit).
'use strict';
const fs = require('fs'), path = require('path');
const NP = require('child_process').execSync('npm root -g').toString().trim();
process.env.NODE_PATH = NP; require('module').Module._initPaths();
const { chromium } = require('playwright');
const DATA = path.join(__dirname, '..', 'data');
const OVER = path.join(DATA, 'broker-phone-overlay.json');
const CDP = 'http://127.0.0.1:18800';
const TABS = parseInt(process.env.TABS || '4', 10);         // parallel tabs in openclaw's Chrome
const CA_AREA = new Set(['209','213','279','310','323','341','350','408','415','424','442','510','530','559','562','619','626','628','650','657','661','669','707','714','747','760','805','818','820','831','858','909','916','925','935','949','951']);
const norm = s => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
const area = p => { const d = String(p||'').replace(/\D/g,'').replace(/^1(?=\d{10}$)/,''); return d.slice(0,3); };
const fmt = 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)}` : ''; };

const firms = JSON.parse(fs.readFileSync('/tmp/miss-firms.json', 'utf8'));
let overlay = fs.existsSync(OVER) ? JSON.parse(fs.readFileSync(OVER, 'utf8')) : {};
const queue = firms.filter(f => f && !overlay['firm:' + norm(f)] && !overlay['firm:' + norm(f) + ':nohit']);
let idx = 0, done = 0, hit = 0;
const saveOverlay = () => fs.writeFileSync(OVER, JSON.stringify(overlay));

async function worker(page, wid) {
  while (idx < queue.length) {
    const firm = queue[idx++];
    const key = 'firm:' + norm(firm);
    try {
      const q = encodeURIComponent(firm + ' commercial real estate Los Angeles phone number');
      await page.goto('https://www.google.com/search?q=' + q, { waitUntil: 'domcontentloaded', timeout: 25000 });
      await page.waitForTimeout(700 + Math.floor((wid + idx) % 5) * 120);
      const phone = await page.evaluate(() => {
        const t = document.body.innerText;
        // prefer the knowledge-panel "Phone:" line, else first CA-looking number
        const kp = (t.match(/Phone:\s*(\(?\d{3}\)?[ .\-]\d{3}[ .\-]\d{4})/i) || [])[1];
        if (kp) return kp;
        const all = t.match(/\(?\d{3}\)?[ .\-]\d{3}[ .\-]\d{4}/g) || [];
        return all[0] || '';
      });
      const ph = fmt(phone);
      if (ph && CA_AREA.has(area(ph))) { overlay[key] = { phone: ph, firm, tier: 'firm', src: 'google-openclaw' }; hit++; }
      else { overlay[key + ':nohit'] = 1; }          // mark so we don't re-try a dry firm
    } catch (_) { /* transient — leave unmarked to retry next run */ }
    if (++done % 8 === 0) { saveOverlay(); process.stdout.write(`  ${done}/${queue.length} firms (${hit} phones)\r`); }
    await page.waitForTimeout(400);
  }
}

(async () => {
  console.log(`Crexi firm-phone backfill: ${queue.length} firms, ${TABS} parallel openclaw tabs ($0). All-day.`);
  const browser = await chromium.connectOverCDP(CDP);
  const ctx = browser.contexts()[0] || await browser.newContext();
  const pages = [];
  for (let i = 0; i < TABS; i++) pages.push(await ctx.newPage());
  await Promise.all(pages.map((p, i) => worker(p, i)));
  for (const p of pages) await p.close().catch(() => {});
  saveOverlay();
  console.log(`\nDone: ${done} firms resolved, ${hit} firm phones added to overlay.`);
})().catch(e => { saveOverlay(); console.error('backfill-crexi-firms FAILED:', e.message); process.exit(1); });