fix(tradein): канон срезает гео-префиксы (Россия/Екатеринбург/мкр/пр-т) + REINDEX — покрытие 38%→~70% (147) (#2073)
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 31s
Deploy Trade-In / test (push) Successful in 1m33s
Deploy Trade-In / build-backend (push) Successful in 27s
Deploy Trade-In / deploy (push) Successful in 53s

This commit is contained in:
lekss361 2026-06-28 17:54:26 +00:00
parent eab3d928d6
commit b45a819cb6

View file

@ -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 не срезала
-- (срезался только «р-н ...,»):
-- «Россия,», «Свердловская обл./область,», «г Екатеринбург,»/«Екатеринбург,»,
-- «мкр. Эльмаш,», «<Имя> м-н,» (суффикс-маркер микрорайона — «Химмаш м-н»),
-- «пос./посёлок ...,», «жилой район ...,», «снт/пгт ...,»,
-- ПЛЮС баг проспекта: «пр-т»/«пр-кт» → старый токен-срез убирал «пр», оставляя хвост
-- «т»/«кт» (а ГАР хранит «пр-кт Космонавтов» с тем же багом → обе стороны не совпадали).
-- Аналогичный баг у «б-р» (бульвар) и маркера дома «д.»/«д <N>».
--
-- 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 <name> допустим внутри транзакции (в отличие
-- от 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;