-- 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;