← back to Ventura Corridor
db/migrations/014_building_dupes.sql
70 lines
-- Migration 014 — same-tenant detection within a building
-- Surfaces business pairs that are likely the same entity in different suites:
-- 1. Name similarity > 0.5 via pg_trgm (e.g. "ABRAMS LAW LLC" vs "ABRAMS & ASSOC LAW")
-- 2. Shared phone number (different suite, same line)
-- 3. Shared website domain
-- Materialized so /api/buildings/:bldg/dupes is fast even on big buildings (575 tenants at 15821).
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE OR REPLACE VIEW v_business_normalized AS
SELECT
bc.id, bc.name, bc.address, bc.suite, bc.bldg_address, bc.city, bc.zip,
bc.phone, bc.website,
-- Normalize name: strip LLC/INC/CORP/CO/LTD/LP suffixes; collapse whitespace; lowercase
LOWER(TRIM(regexp_replace(
bc.name,
'\s+(LLC|L\.L\.C\.|INC|INC\.|INCORPORATED|CORP|CORP\.|CORPORATION|CO|CO\.|COMPANY|LTD|LTD\.|LP|LLP|PROFESSIONAL CORPORATION|PC)\.?$',
'', 'i'
))) AS name_norm,
-- Strip phone formatting: "(818) 555-0123" → "8185550123"
regexp_replace(COALESCE(bc.phone, ''), '\D', '', 'g') AS phone_digits,
-- Strip website to bare domain: "https://www.abramslaw.com/about" → "abramslaw.com"
LOWER(regexp_replace(
regexp_replace(COALESCE(bc.website, ''), '^https?://(www\.)?', '', 'i'),
'/.*$', ''
)) AS website_domain
FROM v_building_canonical bc;
DROP MATERIALIZED VIEW IF EXISTS mv_building_dupes;
CREATE MATERIALIZED VIEW mv_building_dupes AS
SELECT DISTINCT
bn1.bldg_address,
bn1.id AS id_a,
bn2.id AS id_b,
bn1.name AS name_a,
bn2.name AS name_b,
bn1.suite AS suite_a,
bn2.suite AS suite_b,
CASE
WHEN bn1.phone_digits = bn2.phone_digits AND bn1.phone_digits <> '' THEN 'phone'
WHEN bn1.website_domain = bn2.website_domain AND bn1.website_domain <> '' THEN 'website'
ELSE 'name'
END AS match_type,
GREATEST(
similarity(bn1.name_norm, bn2.name_norm),
CASE WHEN bn1.phone_digits = bn2.phone_digits AND bn1.phone_digits <> '' THEN 1.0 ELSE 0 END,
CASE WHEN bn1.website_domain = bn2.website_domain AND bn1.website_domain <> '' THEN 1.0 ELSE 0 END
)::numeric(3,2) AS confidence
FROM v_business_normalized bn1
JOIN v_business_normalized bn2
ON bn1.bldg_address = bn2.bldg_address
AND bn1.id < bn2.id -- avoid pairing a row with itself; produce each pair once
AND bn1.bldg_address IS NOT NULL
AND (
similarity(bn1.name_norm, bn2.name_norm) > 0.5
OR (bn1.phone_digits = bn2.phone_digits AND bn1.phone_digits <> '')
OR (bn1.website_domain = bn2.website_domain AND bn1.website_domain <> '')
);
CREATE INDEX IF NOT EXISTS idx_mv_dupes_bldg ON mv_building_dupes (bldg_address);
CREATE INDEX IF NOT EXISTS idx_mv_dupes_a ON mv_building_dupes (id_a);
CREATE INDEX IF NOT EXISTS idx_mv_dupes_b ON mv_building_dupes (id_b);
-- Trigger-style refresh helper (call via /api/buildings/refresh-dupes)
CREATE OR REPLACE FUNCTION refresh_building_dupes() RETURNS void AS $$
BEGIN
REFRESH MATERIALIZED VIEW mv_building_dupes;
END;
$$ LANGUAGE plpgsql;