[object Object]

← back to Nationalrealestate

resolver: confidence-tier matches — only set firm.website when host echoes the name; loose matches recorded as low_confidence leads (audited ~30% loose), don't overwrite website with a maybe-wrong site

7241fde27663949aa1c6c82a88dcc0dce2ad5b99 · 2026-07-30 19:18:24 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 7241fde27663949aa1c6c82a88dcc0dce2ad5b99
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Thu Jul 30 19:18:24 2026 -0700

    resolver: confidence-tier matches — only set firm.website when host echoes the name; loose matches recorded as low_confidence leads (audited ~30% loose), don't overwrite website with a maybe-wrong site
---
 src/enrich/firm_website_resolve.ts | 36 +++++++++++++++++++++++++-----------
 1 file changed, 25 insertions(+), 11 deletions(-)

diff --git a/src/enrich/firm_website_resolve.ts b/src/enrich/firm_website_resolve.ts
index 50a9711..567a778 100644
--- a/src/enrich/firm_website_resolve.ts
+++ b/src/enrich/firm_website_resolve.ts
@@ -91,8 +91,14 @@ async function searchText(q: string): Promise<Array<{ name: string; website?: st
   return (j.places || []).map((p: any) => ({ name: p.displayName?.text || '', website: p.websiteUri }));
 }
 
-/** Best own-site pick: skip aggregators; prefer a host that echoes the firm name. */
-function pickWebsite(hits: Array<{ name: string; website?: string }>, firmName: string): string | null {
+/**
+ * Best own-site pick: skip aggregators; prefer a host that echoes the firm name.
+ * Returns `confident=true` only when the winning host echoes the name — Places
+ * sometimes returns an agent's personal page or a franchise-brand portal for a
+ * firm, so a non-echoing host is recorded as a LEAD (firm_site low_confidence)
+ * WITHOUT overwriting firm.website. ~30% of hits are loose (audited 2026-07-30).
+ */
+function pickWebsite(hits: Array<{ name: string; website?: string }>, firmName: string): { url: string; confident: boolean } | null {
   const cands = hits
     .map(h => h.website).filter((u): u is string => !!u)
     .filter(u => { const h = hostOf(u); return h && !BLOCK.has(h); });
@@ -100,7 +106,8 @@ function pickWebsite(hits: Array<{ name: string; website?: string }>, firmName:
   const named = cands.find(u => { const h = hostOf(u); return h && nameEchoesHost(firmName, h); });
   const winner = named || cands[0];
   const h = hostOf(winner);
-  return h ? 'https://' + h : null;
+  if (!h) return null;
+  return { url: 'https://' + h, confident: !!named };
 }
 
 interface FirmRow { id: number; name: string; hq_city: string | null; license_state: string | null }
@@ -141,20 +148,27 @@ async function main() {
   if (budget <= 0) { console.log(`[resolve] monthly cap ${CAP} reached — HARD STOP, $0 spent`); await pool.end(); return; }
 
   const runId = await openRun(SOURCE, 'places-firm-resolve');
-  let calls = 0, resolved = 0, noSite = 0, fatal: any = null;
+  let calls = 0, resolved = 0, lowConf = 0, noSite = 0, fatal: any = null;
   try {
     for (const f of queue) {
       if (calls >= budget) { console.log('[resolve] cap reached mid-run — stopping'); break; }
       const q = `${f.name} ${f.hq_city || ''} ${f.license_state || ''}`.replace(/\s+/g, ' ').trim();
       const hits = await searchText(q);
       calls++; await bumpQuota(1); logCost(1, `resolve "${f.name.slice(0, 40)}"`);
-      const website = pickWebsite(hits, f.name);
-      if (website) {
+      const pick = pickWebsite(hits, f.name);
+      if (pick && pick.confident) {
         resolved++;
-        await query(`UPDATE firm SET website = COALESCE(website, $2) WHERE id = $1`, [f.id, website]);
+        await query(`UPDATE firm SET website = COALESCE(website, $2) WHERE id = $1`, [f.id, pick.url]);
         await query(
           `INSERT INTO firm_site (firm_id, url, discovery_method) VALUES ($1,$2,'google_places_resolve')
-           ON CONFLICT (firm_id) DO NOTHING`, [f.id, website]);
+           ON CONFLICT (firm_id) DO NOTHING`, [f.id, pick.url]);
+      } else if (pick) {
+        // Loose match (host doesn't echo the firm name) — record the LEAD for
+        // review/crawl but do NOT overwrite firm.website with a maybe-wrong site.
+        lowConf++;
+        await query(
+          `INSERT INTO firm_site (firm_id, url, discovery_method, crawl_status)
+           VALUES ($1,$2,'google_places_resolve','low_confidence') ON CONFLICT (firm_id) DO NOTHING`, [f.id, pick.url]);
       } else {
         noSite++;
         await query(
@@ -170,10 +184,10 @@ async function main() {
   // Fail-soft: always close the run (never leave a zombie 'running').
   const status = calls > 0 ? 'ok' : 'failed';
   await closeRun(runId, status, {
-    upserted: resolved, skipped: noSite,
-    notes: `${calls} Places calls, ${resolved} sites resolved, ${noSite} no-site${fatal ? ` · partial(${String(fatal?.message || fatal).slice(0, 40)})` : ''}`,
+    upserted: resolved, skipped: lowConf + noSite,
+    notes: `${calls} Places calls, ${resolved} confident sites, ${lowConf} low-confidence leads, ${noSite} no-site${fatal ? ` · partial(${String(fatal?.message || fatal).slice(0, 40)})` : ''}`,
   });
-  console.log(`[resolve] done: ${calls} calls · ${resolved} sites resolved · ${noSite} no-site · $0 (free-tier)${fatal ? ' (partial)' : ''}`);
+  console.log(`[resolve] done: ${calls} calls · ${resolved} confident · ${lowConf} low-confidence · ${noSite} no-site · $0 (free-tier)${fatal ? ' (partial)' : ''}`);
   await pool.end();
 }
 

← 0846541 Add Places-API firm->website resolver (resolve:firms): relia  ·  back to Nationalrealestate  ·  Add Weichert listings adapter (3rd homes source) + fix extra 1bf4cc4 →