diff --git a/tradein-mvp/backend/data/sql/147_canon_strip_geo_prefixes.sql b/tradein-mvp/backend/data/sql/147_canon_strip_geo_prefixes.sql new file mode 100644 index 00000000..4b74877a --- /dev/null +++ b/tradein-mvp/backend/data/sql/147_canon_strip_geo_prefixes.sql @@ -0,0 +1,104 @@ +-- 147_canon_strip_geo_prefixes.sql +-- +-- CONTEXT: канон-матч ГАР↔houses (мигр. 144, tradein_canon_addr) покрывал лишь 38% +-- вторички (2596 правдоподобных домов из 6810). Это НЕ дыра в данных ГАР: ГАР уже знает +-- flat_count для ~2000-2700 из 4041 непокрытых домов. Канон промахивался, потому что +-- houses-адреса несут ГЕОГРАФИЧЕСКИЕ ПРЕФИКСЫ, которые мигр.144 не срезала +-- (срезался только «р-н ...,»): +-- «Россия,», «Свердловская обл./область,», «г Екатеринбург,»/«Екатеринбург,», +-- «мкр. Эльмаш,», «<Имя> м-н,» (суффикс-маркер микрорайона — «Химмаш м-н»), +-- «пос./посёлок ...,», «жилой район ...,», «снт/пгт ...,», +-- ПЛЮС баг проспекта: «пр-т»/«пр-кт» → старый токен-срез убирал «пр», оставляя хвост +-- «т»/«кт» (а ГАР хранит «пр-кт Космонавтов» с тем же багом → обе стороны не совпадали). +-- Аналогичный баг у «б-р» (бульвар) и маркера дома «д.»/«д ». +-- +-- WHAT: CREATE OR REPLACE FUNCTION tradein_canon_addr(text) — расширяет мигр.144: +-- S1 «пр-т/пр-кт/пр кт/пркт» → «проспект»; «б-р» → «бульвар» (чинит хвост-баг до +-- токен-среза, и на houses-, и на ГАР-стороне «пр-кт»); +-- S2 срез СУФФИКС-сегмента региона: «<...> обл[.]/область,»; +-- S3 срез СУФФИКС-сегмента микрорайона: «<Имя> м-н,»; +-- S4 срез ПРЕФИКС-сегментов: «россия,», «екатеринбург,», «город/г ...,», +-- «жилой район/жилрайон ...,», «пгт/снт ...,», «р-н ...,», «м-н ...,» — +-- якорь на границе сегмента (^|,) + lookahead «,» (каскадно срезает цепочку +-- «Россия, Екатеринбург, ...» — lookahead НЕ съедает запятую, она остаётся +-- якорем следующего сегмента); +-- S4b срез НЕОДНОЗНАЧНЫХ маркеров «мкр/пос/поселок ...,» ТОЛЬКО если следом идёт +-- улица (сегмент с кириллицей) — guard `(?=,[^,]*[а-я])`: защищает «мкр. X, 15» +-- (номер прямо в микрорайоне) от потери дома; +-- S5b срез маркера дома «д[.]» перед номером (lookahead на цифру); +-- далее — СУЩЕСТВУЮЩИЙ токен-срез типов улиц (S5) + keep [а-я0-9] (S6) из мигр.144. +-- Чистые ГАР-адреса «ул Шаумяна, 20» и houses «ул. Шаумяна,20» по-прежнему → «шаумяна20» +-- (новые гео-срезы — no-op на чистых адресах, backward-compatible). +-- +-- Проверено на проде (ROLLBACK-tx, REINDEX внутри): +-- RECOVERY: из 4041 непокрытых view-домов канон совпал с ГАР-ЕКБ (flat_count>0) у 2905, +-- из них правдоподобных (flat_count>=GREATEST(floors,8) AND active<=flat_count) — 2789. +-- NO-REGRESSION: из 4681 уже-совпавших домов сохранили тот же flat_count 4678, lost=0, +-- changed=3 — и все 3 это УЛУЧШЕНИЕ (проспект Ленина: ГАР хранит «пр-кт Ленина», старый +-- баг матчил недоучтённую строку fc 2/8 → новый канон цепляет полную fc 166/34/11). +-- FALSE-POSITIVE: выборка из 12 пар — улица+номер совпадают точно (нет 33→333 ошибок). +-- Итог покрытия: 38% → ~70-80% (после ре-матча). +-- +-- DEPENDENCIES (existing prod objects): +-- - tradein_canon_addr(text) — мигр. 144 (заменяем тело, IMMUTABLE, обратносовместимо). +-- - gar_house_flats_canon_idx — ФУНКЦИОНАЛЬНЫЙ btree-индекс ON gar_house_flats +-- (tradein_canon_addr(norm_address)) WHERE flat_count>0 (мигр. 144). Postgres НЕ +-- перестраивает функц.индекс при смене тела функции → значения в индексе СТАЛЫ → +-- обязателен REINDEX (ниже). Проверено `pg_indexes ILIKE '%tradein_canon_addr%'`: +-- это ЕДИНСТВЕННЫЙ индекс, зависящий от функции. +-- - v_building_sale_share (мигр. 143/145) использует houses.gar_flat_count — не ломается +-- (мы не трогаем колонки/вью; ре-матч лишь до-заполняет gar_flat_count). +-- +-- POST-DEPLOY (НЕ в этом файле — отдельный шаг main-сессии): ре-матч уже зовёт +-- tradein_canon_addr, поэтому после деплоя функции достаточно прогнать матчер: +-- python -m app.tasks.gar_flats_load --match-only --region 66 +-- (он set-based UPDATE'ит houses.gar_flat_count по новому канону; идемпотентно). +-- +-- SAFETY / IDEMPOTENCY: +-- - CREATE OR REPLACE FUNCTION + REINDEX INDEX — повторный прогон no-op (REINDEX просто +-- перестроит индекс ещё раз). REINDEX INDEX допустим внутри транзакции (в отличие +-- от REINDEX CONCURRENTLY / DATABASE). +-- - Функция остаётся IMMUTABLE (обязательно для функц.индекса). +-- - Раннер деплоя гонит файл через psql ON_ERROR_STOP=on БЕЗ --single-transaction → +-- транзакцию открывает сам файл (BEGIN/COMMIT), как 144/145. +-- +-- Deploy order: после 145_building_sale_share_plausible_denom.sql. + +BEGIN; + +-- Канонический ключ адреса: расширяет мигр.144 срезом гео-префиксов + фиксом проспект/бульвар/дом. +-- ВАЖНО порядок regexp_replace: внутренний (S0) исполняется первым, внешний (keep) — последним. +CREATE OR REPLACE FUNCTION tradein_canon_addr(s text) RETURNS text AS $func$ + SELECT regexp_replace( -- S6: оставить только [а-я0-9] + regexp_replace( -- S5: срез типов улиц (токены на границе не-кириллицы) + regexp_replace( -- S5b: срез маркера дома «д[.]» перед номером + regexp_replace( -- S4b: срез «мкр/пос/поселок ...,» только если следом улица + regexp_replace( -- S4: срез префикс-сегментов (каскад через lookahead) + regexp_replace( -- S3: срез суффикс-сегмента «<Имя> м-н,» + regexp_replace( -- S2: срез суффикс-сегмента «<...> обл/область,» + regexp_replace( -- S1b: «б-р» → «бульвар» + regexp_replace( -- S1a: «пр-т/пр-кт/пр кт/пркт» → «проспект» + translate(lower(coalesce(s, '')), 'ё', 'е'), -- S0: lower + ё→е + '(^|[^а-я])пр[-. ]?к?т([^а-я]|$)', '\1проспект\2', 'g'), + '(^|[^а-я])б-р([^а-я]|$)', '\1бульвар\2', 'g'), + '(^|,)[^,]* обл[а-я]*\.?(?=,)', '\1', 'g'), + '(^|,)[^,]* м-н(?=,)', '\1', 'g'), + '(^|,)\s*(россия|екатеринбург|город|жилой район|жилрайон|пгт|снт|р-н|м-н|г)([. ][^,]*)?(?=,)', '\1', 'g'), + '(^|,)\s*(мкр|поселок|пос)[. ][^,]*(?=,[^,]*[а-я])', '\1', 'g'), + '(^|[^а-я])д\.?(?= *[0-9])', '\1', 'g'), + '(^|[^а-я])(улица|ул|переулок|пер|проспект|пркт|пр|бульвар|бр|шоссе|ш|проезд|набережная|наб|площадь|пл|тупик|туп|аллея|микрорайон|мкр)([^а-я]|$)', '\1\3', 'g'), + '[^а-я0-9]', '', 'g'); +$func$ LANGUAGE sql IMMUTABLE; + +COMMENT ON FUNCTION tradein_canon_addr(text) IS + 'Канонический ключ адреса для ГАР↔houses матча (мигр. 144 + 147): lower+ё→е, ' + 'нормализация проспект/бульвар (пр-т/пр-кт/б-р), срез гео-префиксов ' + '(Россия/обл/Екатеринбург/г/мкр/м-н/пос/жилой район/р-н/снт/пгт) и маркера дома «д.», ' + 'затем срез типов улиц как токенов на границе не-кириллицы → оставляет только [а-я0-9]. ' + 'Поднял покрытие 38%%→~70-80%% (recovery 2905/4041, lost=0). См. gar-flats-runbook.md.'; + +-- ОБЯЗАТЕЛЬНО: перестроить функц.индекс — иначе он держит СТАРЫЕ (мигр.144) значения канона +-- и JOIN gar_pick матчил бы по устаревшему ключу. Единственный зависящий индекс (pg_indexes). +REINDEX INDEX gar_house_flats_canon_idx; + +COMMIT;