[object Object]

← back to Commercialrealestate

broker-email: poison-guard sole-email on corporate megasite/office pages (mis-assigns stranger emails) + log-parse committer with rollback

beb4036e805c5fa5e15a15c1bbad1ba0158b5a44 · 2026-07-12 11:45:14 -0700 · Steve Abrams

Files touched

Diff

commit beb4036e805c5fa5e15a15c1bbad1ba0158b5a44
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 11:45:14 2026 -0700

    broker-email: poison-guard sole-email on corporate megasite/office pages (mis-assigns stranger emails) + log-parse committer with rollback
---
 scripts/broker-email-commit-from-logs.js | 69 ++++++++++++++++++++++++++++++++
 scripts/broker-email-finder.js           |  7 +++-
 2 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/scripts/broker-email-commit-from-logs.js b/scripts/broker-email-commit-from-logs.js
new file mode 100644
index 0000000..2a774b0
--- /dev/null
+++ b/scripts/broker-email-commit-from-logs.js
@@ -0,0 +1,69 @@
+#!/usr/bin/env node
+/*
+ * broker-email-commit-from-logs.js — write the finder's CONFIDENT hits to broker.email by parsing
+ * the dry-run logs (NO re-scrape). Reversible: only fills rows where email IS NULL, and writes a
+ * rollback file (id,prev-null) so it can be undone. Confident bases only (default): name-match,
+ * role-inbox, sole-email (± "+browser"). ambiguous / timeout / anything else is NEVER written.
+ *   node broker-email-commit-from-logs.js <log...> --dry              # preview
+ *   node broker-email-commit-from-logs.js <log...> --commit           # write
+ *   node broker-email-commit-from-logs.js <log...> --commit --bases name-match,role-inbox
+ */
+'use strict';
+const fs = require('fs'); const { execFileSync } = require('child_process');
+const args = process.argv.slice(2);
+const COMMIT = args.includes('--commit');
+const bi = args.indexOf('--bases');
+const BASES = bi > -1 ? args[bi + 1].split(',') : ['name-match', 'role-inbox', 'sole-email'];
+const logs = args.filter(a => a.endsWith('.log') && fs.existsSync(a));
+const psql = s => execFileSync('psql', ['-At', '-d', 'cre', '-c', s], { encoding: 'utf8' }).trim();
+const q = s => "'" + String(s).replace(/'/g, "''") + "'";
+
+// Parse: "✓ Name  (#123)  [basis]" then a following "    EMAIL: addr ..." line.
+// Corporate megasites / office pages: a broker's "website" is often a shared corp office page that
+// renders ONE OTHER agent's email → `sole-email` there mis-assigns a stranger. Drop those.
+const CORP = /marcusmillichap|cbre|kw\.com|kwcommercial|remax|coldwell|century21|compass|colliers|cushwake|jll|berkadia|newmark|kidder\.com/i;
+const OFFICE = /\/(offices?|about-us|team|our-team|agents)\//i;
+function parse(text) {
+  const out = [];
+  const lines = text.split('\n');
+  for (let i = 0; i < lines.length; i++) {
+    const m = lines[i].match(/^✓\s+.*?\(#(\d+)\)\s+\[([a-z+-]+)\]/);
+    if (!m) continue;
+    const id = +m[1], basis = m[2].replace('+browser', '');
+    let site = '', em = null;
+    for (let j = 1; j <= 3 && i + j < lines.length; j++) {
+      if (/^\s+site:/.test(lines[i + j])) site = lines[i + j];
+      const e = lines[i + j].match(/EMAIL:\s+(\S+)/); if (e) { em = e; break; }
+    }
+    if (!em) continue;
+    const email = em[1].toLowerCase();
+    // POISON GUARD: sole-email only trusted on the broker's OWN small domain — never a corp megasite/office page.
+    if (basis === 'sole-email' && (CORP.test(site) || CORP.test(email) || OFFICE.test(site))) continue;
+    out.push({ id, basis, email });
+  }
+  return out;
+}
+
+const seen = new Map();   // id -> {email, basis}  (first confident wins; logs are ordered plain→browser)
+for (const lg of logs) for (const r of parse(fs.readFileSync(lg, 'utf8'))) {
+  if (!BASES.includes(r.basis)) continue;
+  if (!seen.has(r.id)) seen.set(r.id, r);
+}
+const rows = [...seen.values()];
+console.log(`confident hits parsed from ${logs.length} logs (bases: ${BASES.join(',')}): ${rows.length}`);
+const byBasis = {}; rows.forEach(r => byBasis[r.basis] = (byBasis[r.basis] || 0) + 1);
+console.log('  by basis:', JSON.stringify(byBasis));
+
+if (!COMMIT) { console.log('DRY — nothing written. Sample:', rows.slice(0, 5).map(r => `#${r.id} ${r.email} [${r.basis}]`).join(' | ')); process.exit(0); }
+
+let wrote = 0, skipped = 0; const rollback = [];
+for (const r of rows) {
+  // only fill still-empty rows; never overwrite an existing email
+  const cur = psql(`SELECT COALESCE(email,'') FROM broker WHERE id=${r.id};`);
+  if (cur) { skipped++; continue; }
+  psql(`UPDATE broker SET email=${q(r.email)} WHERE id=${r.id} AND (email IS NULL OR email='');`);
+  rollback.push(r.id); wrote++;
+}
+fs.writeFileSync('/tmp/broker-email-commit-rollback.txt', rollback.join(','));
+console.log(`COMMITTED: ${wrote} written to broker.email, ${skipped} skipped (already had email). Rollback ids -> /tmp/broker-email-commit-rollback.txt`);
+console.log(`Undo: psql -d cre -c "UPDATE broker SET email=NULL WHERE id IN (${rollback.slice(0,3).join(',')}...);"`);
diff --git a/scripts/broker-email-finder.js b/scripts/broker-email-finder.js
index 0af7f96..4e80bc1 100644
--- a/scripts/broker-email-finder.js
+++ b/scripts/broker-email-finder.js
@@ -153,8 +153,11 @@ function pickBest(mailtos, texts, dom, name) {
   // 2) a role inbox on the site's own domain (info@/contact@) — the firm's real contact.
   const roleInbox = pool.find(e => role(e) && sameDomain(e, dom));
   if (roleInbox) return { email: roleInbox, basis: 'role-inbox', pool };
-  // 3) exactly ONE email on the whole site → almost certainly this broker's (single-agent site).
-  if (pool.length === 1) return { email: pool[0], basis: 'sole-email', pool };
+  // 3) exactly ONE email on the whole site → this broker's, BUT ONLY on their own small domain.
+  //    On a corporate megasite (marcusmillichap/cbre/kw/…) a broker's "website" is often a shared
+  //    office page rendering ONE OTHER agent's email — sole-email there mis-assigns a stranger. Skip.
+  const CORP_DOM = /marcusmillichap|cbre|kw\.com|kwcommercial|remax|coldwell|century21|compass|colliers|cushwake|jll|berkadia|newmark|kidder/i;
+  if (pool.length === 1 && !CORP_DOM.test(dom) && !CORP_DOM.test(pool[0])) return { email: pool[0], basis: 'sole-email', pool };
   // 4) Multiple PERSONAL emails, none matching this broker → it's a shared/corporate page listing
   //    OTHER agents. Refuse to guess — a stranger's email is worse than none.
   return { email: null, basis: 'ambiguous-shared-page', pool };

← f93ac06 broker-email-finder: hard per-broker timeout so a single hun  ·  back to Commercialrealestate  ·  auto-save: 2026-07-12T11:48:02 (2 files) — data/listings.jso 310925d →