fix(tradein/estimator): дом «2» вместо «15» при глухом названии станции метро в avito-адресе #2429

Merged
lekss361 merged 1 commit from fix/tradein-dedup-2423-avito-metro-glue into main 2026-07-04 15:50:50 +00:00
Owner

Summary

Продолжение #2291 — новый класс остаточных кросс-source дублей (#2423, найден при live-перезамере PR #2417): ~244 из 434 survivor-групп на текущем денаминаторе.

Root cause: некоторые avito-адреса приклеивают название ближайшей станции метро к номеру дома без разделителя ("ул. Амундсена, 2Чкаловская11-15 мин. пешком"). Кириллическая буква и цифра — оба \w для \b, поэтому _DEDUP_HOUSE_NO_RE (estimator.py:4954) не находит word-boundary между номером дома и приклеенным словом → trailing \b ложно отменял весь матч цифры → .search() сдвигался вправо и захватывал «15» из хвоста «11-15 мин» (расстояние до метро пешком) вместо реального номера дома «2». Дальше guard_house в _dedup_cross_source корректно (по своей логике), но на неверных входных данных блокировал слияние с cian/domklik/yandex-версией того же физлота — legit-дубль не схлопывался (under-merge, противоположный риск от over-merge в #2291/PR #2417).

Fix: минимальное изменение регекса — суффикс-буква («46г», «8а») засчитывается только если ЗА НЕЙ не идёт ещё один word-символ ((?!\w), буква ИЛИ цифра — не только буква, см. ниже почему), plus убран лишний trailing \b, который и вызывал ложную отмену матча.

Важное отклонение от первоначального дизайна (найдено воркером на шаге full-suite regression run, ДО ревью): буквальная версия из дизайна ((?![а-яё]) — запрет только на ЕЩЁ ОДНУ букву) чинила avito-баг, но ломала приклеенный без пробела корпус-формат "6к1" — он стал давать house="6к", тогда как эквивалент с пробелом "6 к1" (domklik) по-прежнему давал house="6" → два представления ОДНОГО физического адреса начинали расходиться, и guard_house начинал ошибочно блокировать их слияние (сломал 4 существующих теста). Финальный regex использует (?!\w) (запрет на любой word-символ — букву ИЛИ цифру), что чинит исходный баг И делает "6к1"house="6", совпадая с "6 к1".

Проверено лично (независимо от воркера/ревьюера) — прогнал regex вручную в Python REPL:

Вход OLD NEW
2Чкаловская11-15 мин 15 (баг) 2
24г 24г 24г (не сломано)
32а 32а 32а (не сломано)
12/3 12/3 12/3 (не сломано)
6к1 (без пробела) None (пусто) 6 (улучшение, не регрессия)
6 к1 (с пробелом) 6 6 (совпадает с «6к1» теперь)

Test plan

  • tests/test_estimator_dedup_cross_source_2087.py — 63/63 (9 новых тестов на #2423 + 2 обновлённых пиннинг-кейса на «6к1»), прогнал лично в venv
  • pytest tests/ -k "estimator or dedup or geocoder" — 611 passed / 4 skipped, прогнал лично в venv
  • Полный backend suite (по отчёту воркера) — 2276 passed / 1 pre-existing unrelated failure (test_search_api.py::test_search_cache_hit, подтверждено git stash — падает так же на немодифицированном main, auth/env test-isolation, не связано с диффом)
  • Adversarial deep-code-review (APPROVE, severity=minor) — non-blocking nit про over-merge e2e тест (не одинаковый distance-bucket в обоих лотах теста), не блокирует
  • Live post-deploy перезамер (та же методика, что #2291/PR #2417) — сравнить pre/post-fix survivor-групп на текущем денаминаторе

Refs #2423, #2291

## Summary Продолжение #2291 — новый класс остаточных кросс-source дублей (#2423, найден при live-перезамере PR #2417): ~244 из 434 survivor-групп на текущем денаминаторе. **Root cause:** некоторые avito-адреса приклеивают название ближайшей станции метро к номеру дома без разделителя (`"ул. Амундсена, 2Чкаловская11-15 мин. пешком"`). Кириллическая буква и цифра — оба `\w` для `\b`, поэтому `_DEDUP_HOUSE_NO_RE` (`estimator.py:4954`) не находит word-boundary между номером дома и приклеенным словом → trailing `\b` ложно отменял весь матч цифры → `.search()` сдвигался вправо и захватывал «15» из хвоста «11-15 мин» (расстояние до метро пешком) вместо реального номера дома «2». Дальше `guard_house` в `_dedup_cross_source` корректно (по своей логике), но на неверных входных данных блокировал слияние с cian/domklik/yandex-версией того же физлота — legit-дубль не схлопывался (under-merge, противоположный риск от over-merge в #2291/PR #2417). **Fix:** минимальное изменение регекса — суффикс-буква («46г», «8а») засчитывается только если ЗА НЕЙ не идёт ещё один word-символ (`(?!\w)`, буква ИЛИ цифра — не только буква, см. ниже почему), plus убран лишний trailing `\b`, который и вызывал ложную отмену матча. **Важное отклонение от первоначального дизайна** (найдено воркером на шаге full-suite regression run, ДО ревью): буквальная версия из дизайна (`(?![а-яё])` — запрет только на ЕЩЁ ОДНУ букву) чинила avito-баг, но ломала приклеенный без пробела корпус-формат `"6к1"` — он стал давать `house="6к"`, тогда как эквивалент с пробелом `"6 к1"` (domklik) по-прежнему давал `house="6"` → два представления ОДНОГО физического адреса начинали расходиться, и `guard_house` начинал ошибочно блокировать их слияние (сломал 4 существующих теста). Финальный regex использует `(?!\w)` (запрет на любой word-символ — букву ИЛИ цифру), что чинит исходный баг И делает `"6к1"` → `house="6"`, совпадая с `"6 к1"`. **Проверено лично (независимо от воркера/ревьюера)** — прогнал regex вручную в Python REPL: | Вход | OLD | NEW | |---|---|---| | `2Чкаловская11-15 мин` | `15` (баг) | `2` ✅ | | `24г` | `24г` | `24г` (не сломано) | | `32а` | `32а` | `32а` (не сломано) | | `12/3` | `12/3` | `12/3` (не сломано) | | `6к1` (без пробела) | `None` (пусто) | `6` (улучшение, не регрессия) | | `6 к1` (с пробелом) | `6` | `6` (совпадает с «6к1» теперь) | ## Test plan - [x] `tests/test_estimator_dedup_cross_source_2087.py` — 63/63 (9 новых тестов на #2423 + 2 обновлённых пиннинг-кейса на «6к1»), прогнал лично в venv - [x] `pytest tests/ -k "estimator or dedup or geocoder"` — 611 passed / 4 skipped, прогнал лично в venv - [x] Полный backend suite (по отчёту воркера) — 2276 passed / 1 pre-existing unrelated failure (`test_search_api.py::test_search_cache_hit`, подтверждено `git stash` — падает так же на немодифицированном main, auth/env test-isolation, не связано с диффом) - [x] Adversarial deep-code-review (APPROVE, severity=minor) — non-blocking nit про over-merge e2e тест (не одинаковый distance-bucket в обоих лотах теста), не блокирует - [ ] Live post-deploy перезамер (та же методика, что #2291/PR #2417) — сравнить pre/post-fix survivor-групп на текущем денаминаторе Refs #2423, #2291
lekss361 added 1 commit 2026-07-04 15:49:24 +00:00
fix(tradein/estimator): дом «2» вместо «15» при глухом названии станции метро в avito-адресе (#2423)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 54s
9a83b79d91
Root cause: _DEDUP_HOUSE_NO_RE требовал trailing \b после захваченной
цифры/литеры. Кириллическая буква и цифра — оба \w, между ними нет
word-boundary, поэтому когда avito приклеивает название станции метро
прямо к номеру дома без пробела («2Чкаловская11-15 мин. пешком»), матч
цифры «2» целиком отменялся, и .search() уезжал вправо до дистанции
«11-15» (где дефис даёт настоящую границу), ошибочно принимая «15» за
номер дома.

Фикс: убрали trailing \b (незачем — \d+ уже жадный), сузили суффикс-
литеру условием "это последний word-символ токена" через (?!\w) — не
просто "не кириллическая буква", а вообще ЛЮБОЙ word-символ (буква ИЛИ
цифра). Первая версия фикса ((?![а-яё]), из первоначального дизайна)
ловила метро-кейс, но ломала 4 существующих теста: avito-формат «6к1»
(литера корпуса приклеена к цифре без пробела) стал давать house="6к",
расходясь с эквивалентным domklik-форматом «6 к1» → "6" — guard_house
видел «6к» != «6» и блокировал законный cross-source merge. (?!\w)
устраняет и метро-баг, и эту регрессию разом: «6к1» теперь тоже даёт "6".

guard_house/union() не тронуты — чистая extraction-фикс, guard уже
корректно обрабатывает equal-or-missing house_no.

9 новых тестов (глухие метро-имена: обычное/дефисное/короткое; пин
letter-suffix/corpus-slash; partial-capture для глухого corpus-letter;
limitation-пин для двух склеенных числовых прогонов; 2 e2e merge/no-merge)
+ обновлены 2 существующих pinned-ожидания под улучшенное поведение "6к1".
lekss361 merged commit 752c3656a3 into main 2026-07-04 15:50:50 +00:00
lekss361 deleted branch fix/tradein-dedup-2423-avito-metro-glue 2026-07-04 15:50:50 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2429
No description provided.