[object Object]

← back to Ventura Corridor

feat: apply_exa_websites.ts — merge Exa-discovered website JSON into businesses table w/ aggregator filter (yelp/fb/yp/ig blocked)

fd03767bff4fd57ba1186ac76c61d2d80a82af91 · 2026-05-07 12:40:06 -0700 · SteveStudio2

Files touched

Diff

commit fd03767bff4fd57ba1186ac76c61d2d80a82af91
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 7 12:40:06 2026 -0700

    feat: apply_exa_websites.ts — merge Exa-discovered website JSON into businesses table w/ aggregator filter (yelp/fb/yp/ig blocked)
---
 src/enrich/apply_exa_websites.ts | 60 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 60 insertions(+)

diff --git a/src/enrich/apply_exa_websites.ts b/src/enrich/apply_exa_websites.ts
new file mode 100644
index 0000000..46618b8
--- /dev/null
+++ b/src/enrich/apply_exa_websites.ts
@@ -0,0 +1,60 @@
+// Apply Exa-discovered websites to the businesses table.
+//
+// Reads a JSON array (file or stdin) of `[{ id, name, website, source }, ...]`,
+// validates each URL (must be http/https, must NOT be yelp/facebook/yellowpages
+// /bbb/manta/google.com — those are aggregators not primary websites),
+// then UPDATEs businesses.website where currently null.
+//
+// Run:
+//   npx tsx src/enrich/apply_exa_websites.ts /tmp/exa_results.json
+//   curl ... | npx tsx src/enrich/apply_exa_websites.ts -
+
+import * as fs from 'node:fs';
+import { pool, query } from '../../db/pool.ts';
+
+const AGGREGATORS = /^(www\.)?(yelp|facebook|instagram|twitter|x|linkedin|yellowpages|bbb|manta|trustpilot|opentable|grubhub|doordash|ubereats|google|maps\.google|tripadvisor|nextdoor|whitepages|spokeo)\.(com|net|org)$/i;
+
+function normalize(raw: string): string | null {
+  if (!raw) return null;
+  let u = raw.trim();
+  if (!u) return null;
+  if (!/^https?:\/\//i.test(u)) u = 'https://' + u;
+  let parsed: URL;
+  try { parsed = new URL(u); } catch { return null; }
+  if (AGGREGATORS.test(parsed.host)) return null;
+  // Strip query strings and fragments — keep path so brand chain pages survive
+  return parsed.protocol + '//' + parsed.host + parsed.pathname.replace(/\/+$/, '');
+}
+
+async function main() {
+  const path = process.argv[2];
+  if (!path) {
+    console.error('usage: apply_exa_websites.ts <file.json | ->');
+    process.exit(1);
+  }
+  const raw = path === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(path, 'utf8');
+  // Tolerate the agent wrapping the JSON in markdown fences or commentary.
+  const m = raw.match(/\[[\s\S]*\]/);
+  if (!m) { console.error('no JSON array found in input'); process.exit(1); }
+  const rows: Array<{ id: number; name: string; website: string | null; source?: string }> = JSON.parse(m[0]);
+  console.log(`[apply_exa] ${rows.length} candidates`);
+
+  let updated = 0, skipped_null = 0, skipped_invalid = 0, skipped_existing = 0;
+  for (const r of rows) {
+    if (!r.website) { skipped_null++; continue; }
+    const norm = normalize(r.website);
+    if (!norm) { skipped_invalid++; console.log(`  ✕ ${r.id} ${r.name} → ${r.website} (rejected)`); continue; }
+
+    const existing = await query<{ website: string | null }>(`SELECT website FROM businesses WHERE id = $1`, [r.id]);
+    if (!existing.rowCount) { console.log(`  ? ${r.id} not found`); continue; }
+    if (existing.rows[0].website) { skipped_existing++; continue; }
+
+    await query(`UPDATE businesses SET website = $1, last_seen_at = NOW() WHERE id = $2`, [norm, r.id]);
+    updated++;
+    console.log(`  ✓ ${r.id} ${r.name.slice(0, 40).padEnd(40)} → ${norm} [${r.source || '?'}]`);
+  }
+  console.log(`[apply_exa] done · ${updated} updated · ${skipped_null} null · ${skipped_invalid} invalid · ${skipped_existing} already had website`);
+  await pool.end();
+}
+
+main().catch(e => { console.error(e); process.exit(1); });

← 8afd939 feat(news): per-business news rail on map detail panel + new  ·  back to Ventura Corridor  ·  feat(news): news_count badge on /buildings.html tenant rows 0418331 →