gendesign/tradein-mvp/backend/data/sql/124_cad_buildings_local.sql
bot-backend edd187b47f feat(cadastral): geo-nearest building matcher via local cad mirror
Backfill listings.building_cadastral_number (was 0%) from the nearest
cadastral building. gendesign_cad_buildings is a postgres_fdw foreign table
with no geom — a per-listing FDW nearest query is ~1.16s/row (~13h for 43k
listings). Instead materialize the FDW once into a LOCAL cad_buildings_local
table (Point geom + GIST), then run a fast local KNN nearest-neighbour join.

Perf: the distance gate uses geometry-space ST_DWithin(geom, point, deg) (GIST
index, no geography cast) + geom <-> KNN order + ST_DistanceSphere metric
recheck on the single nearest row. A geography-cast ST_DWithin in the WHERE
defeated the index (58s+, full update never finished); the geometry-gate design
does the whole ~41.9k-listing UPDATE in ~5.4s (refresh+match ~7s end-to-end),
matching 16027 listings at 50m across 2229 distinct buildings.

The match is GEO-NEAREST (approximate): a street-level-geocoded listing matches
the nearest building within threshold_m (default 50m), not necessarily its exact
cadastral building. Exact cadastral + parcel-containment deferred (cad_parcels
FDW not exposed). Threshold is logged.

- 124: cad_buildings_local table (empty) + GIST. Migration does not read the FDW
  (deploy-independent); populated by the refresh job.
- 125: scrape_schedules seed source=cadastral_geo_match, enabled, next_run_at
  tomorrow 09:00 UTC (after geocode_missing).
- tasks/cadastral_geo_match.py: refresh_cad_buildings_local (bulk FDW scan),
  match_listings_to_buildings (chunked LATERAL KNN UPDATE), run_cadastral_geo_match
  run-lifecycle wrapper.
- scheduler: trigger_cadastral_geo_match_run + dispatch (sync DB-only, executor).
2026-06-18 10:18:54 +03:00

47 lines
2.2 KiB
PL/PgSQL

-- 124_cad_buildings_local.sql
-- Local materialized mirror of gendesign_cad_buildings (postgres_fdw foreign table)
-- for fast geo-nearest building matching of listings.
--
-- WHY (perf fact, measured live):
-- gendesign_cad_buildings is a postgres_fdw foreign table (~36.7k EKB rows) with NO
-- PostGIS geom — only scalar lat/lon. A per-listing nearest-building query over the FDW
-- takes ~1.16s/row (lat/lon bbox prefilter). With ~43k listings that is ~13h — UNUSABLE.
-- Solution: materialize the FDW once into a LOCAL table with a real Point geom + GIST
-- index, then run a fast local KNN nearest-neighbour join (geom <-> point, GIST-backed).
--
-- This migration creates the EMPTY table + indexes only. It does NOT read the FDW —
-- so deploy never depends on the gendesign DB / FDW server being reachable.
-- The table is populated by the refresh job (app/tasks/refresh_cad_buildings_local.py),
-- driven by the in-app scheduler (source='cadastral_geo_match', combined refresh+match).
--
-- Schedule seed (cadastral_geo_match) lives in 125_scrape_schedules_seed_cadastral_geo_match.sql.
--
-- DEPENDENCIES: PostGIS (postgis extension, present since geom on listings/houses).
-- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS. Safe to re-run.
BEGIN;
CREATE TABLE IF NOT EXISTS cad_buildings_local (
cad_num text PRIMARY KEY,
readable_address text,
year_built int,
floors int,
area_m2 numeric,
purpose text,
lat double precision,
lon double precision,
geom geometry(Point, 4326)
);
COMMENT ON TABLE cad_buildings_local IS
'Local mirror of gendesign_cad_buildings FDW (Rosreestr building registry, EKB ~36.7k). '
'Populated by app/tasks/refresh_cad_buildings_local.py (single bulk FDW scan, TRUNCATE+INSERT). '
'Point geom + GIST enables fast local KNN nearest-building matching '
'(run_cadastral_geo_match) for listings.building_cadastral_number. '
'Migration 124. Refresh+match driven by scheduler source=cadastral_geo_match.';
-- GIST on geom — required for the KNN <-> operator and ST_DWithin predicate.
CREATE INDEX IF NOT EXISTS cad_buildings_local_geom_idx
ON cad_buildings_local USING GIST (geom);
COMMIT;