← back to Ventura Corridor

src/enrich/btrc_match.ts

84 lines

/**
 * btrc_match — populate has_btrc_match for every non-la_btrc business on the corridor.
 * A business is "matched" if any la_btrc row exists at the same street_number ± 5
 * on Ventura Blvd. Otherwise → "POSSIBLY NO BUSINESS LICENSE".
 */
import { query, pool } from '../../db/pool.ts';

async function main() {
  const t0 = Date.now();

  // Reset state
  await query(`UPDATE businesses
    SET has_btrc_match = NULL, btrc_match_score = NULL, btrc_matched_id = NULL, btrc_match_at = NULL
    WHERE on_corridor AND source != 'la_btrc'`);

  // For each non-btrc corridor business, find best btrc match at same street_number (±5).
  // Match score = (street_num_exact ? 1 : 0.7) + name-similarity (trigram, scaled 0..1).
  const sql = `
    WITH non_btrc AS (
      SELECT id, name, street_number, zip,
             NULLIF(regexp_replace(street_number,'[^0-9]','','g'),'')::INT AS sn_int
      FROM businesses
      WHERE on_corridor AND source != 'la_btrc' AND street_number ~ '[0-9]'
    ),
    btrc AS (
      SELECT id, name, street_number, zip,
             NULLIF(regexp_replace(street_number,'[^0-9]','','g'),'')::INT AS sn_int
      FROM businesses
      WHERE on_corridor AND source = 'la_btrc' AND street_number ~ '[0-9]'
    ),
    pairs AS (
      SELECT n.id AS n_id,
             b.id AS b_id,
             CASE WHEN n.sn_int = b.sn_int THEN 1.0
                  WHEN ABS(n.sn_int - b.sn_int) <= 5 THEN 0.7
                  ELSE 0
             END AS addr_score,
             similarity(LOWER(n.name), LOWER(b.name)) AS name_score
      FROM non_btrc n
      JOIN btrc b
        ON ABS(n.sn_int - b.sn_int) <= 5
       AND (n.zip = b.zip OR n.zip IS NULL OR b.zip IS NULL)
    ),
    best AS (
      SELECT n_id, b_id, addr_score, name_score,
             (addr_score * 0.4 + name_score * 0.6) AS score,
             ROW_NUMBER() OVER (PARTITION BY n_id ORDER BY (addr_score * 0.4 + name_score * 0.6) DESC) AS rn
      FROM pairs
      WHERE addr_score > 0
    )
    UPDATE businesses b
       SET has_btrc_match   = (best.score >= 0.5),
           btrc_match_score = best.score,
           btrc_matched_id  = best.b_id,
           btrc_match_at    = NOW()
      FROM best
     WHERE b.id = best.n_id AND best.rn = 1
    RETURNING b.id;
  `;
  const r = await query(sql);
  console.log(`[btrc-match] joined ${r.rowCount} businesses against btrc`);

  // Anything still NULL after the join = no btrc candidate at all → flag false
  const r2 = await query(`
    UPDATE businesses
       SET has_btrc_match = FALSE, btrc_match_at = NOW()
     WHERE on_corridor AND source != 'la_btrc' AND has_btrc_match IS NULL
    RETURNING id`);
  console.log(`[btrc-match] no-btrc-candidate flagged: ${r2.rowCount}`);

  const counts = await query(`
    SELECT
      COUNT(*) FILTER (WHERE on_corridor AND source != 'la_btrc') AS non_btrc_total,
      COUNT(*) FILTER (WHERE on_corridor AND source != 'la_btrc' AND has_btrc_match) AS matched,
      COUNT(*) FILTER (WHERE on_corridor AND source != 'la_btrc' AND has_btrc_match = FALSE) AS flagged
    FROM businesses
  `);
  console.log(`[btrc-match] result:`, counts.rows[0]);
  console.log(`[btrc-match] done in ${Date.now() - t0}ms`);
  await pool.end();
}

main().catch(e => { console.error(e); process.exit(1); });