fix(tradein/estimator): устранить остаточные кросс-сорс дубли (#2291) #2417

Merged
lekss361 merged 2 commits from fix/tradein-dedup-residual-2291 into main 2026-07-04 12:58:16 +00:00
2 changed files with 42 additions and 2 deletions
Showing only changes of commit 0de77690e9 - Show all commits

View file

@ -4987,7 +4987,7 @@ _DEDUP_STREET_TYPE_WORDS_SET = frozenset(_DEDUP_STREET_TYPE_WORDS)
def _bare_street_before_house_segment(s: str) -> tuple[str, str] | None:
"""Bare-street ветка (#2291, класс 1): «<имя>, <номер>» без типа/мкр-слова.
"""Bare-street ветка (#2291, класс 1): «<мкр-сегмент>, <имя>, <номер>».
Root cause: `_DEDUP_MKR_SUFFIX_RE` матчит «<имя> м-н/мкр» ГДЕ УГОДНО в
строке. Для DomClick-формата «Заречный м-н, Готвальда, 24» голая улица
@ -4999,6 +4999,17 @@ def _bare_street_before_house_segment(s: str) -> tuple[str, str] | None:
Разбивает адрес по запятым; ищет ПЕРВЫЙ сегмент, ЦЕЛИКОМ совпадающий с
`_DEDUP_HOUSE_NO_RE` (номер дома). Сегмент НЕПОСРЕДСТВЕННО перед ним 1-3
слова, без мкр-слова/уличного типа/шум-токена кандидат в bare-улицу.
#2291 over-merge guard (deep-review, blocking): ветка активируется ТОЛЬКО
для 3+ сегментного district-chain паттерна обязателен ЕЩЁ более ранний
сегмент (до кандидата-улицы), который сам содержит мкр/район-слово
(`_DEDUP_MKR_WORDS`). Без этого условия голые 2-сегментные адреса без
какого-либо mkr-контекста («ВИЗ, 24», «Академический, 24», «ЖК Некий,
24» частый неформальный ЕКБ-шорткат в объявлениях) фабриковали бы
уличный токен из названия района/ЖК и рисковали ложным cross-merge двух
РАЗНЫХ физлотов. Такие адреса должны вернуть None (решают ветки 1/2/3;
итог безопасный `("", "")`, как и до всей этой ветки).
Не найдено/не подходит None (решают ветки 1/2/3, в частности mkr-fallback
для «мкр Пионерский, 5», где сегмент перед номером содержит мкр-слово).
"""
@ -5010,7 +5021,16 @@ def _bare_street_before_house_segment(s: str) -> tuple[str, str] | None:
hm = _DEDUP_HOUSE_NO_RE.fullmatch(house_seg)
if hm is None:
continue
raw_words = segments[idx - 1].strip().split()
street_idx = idx - 1
earlier_segments = segments[:street_idx]
if not earlier_segments or not any(
any(w in _DEDUP_MKR_WORDS for w in seg.strip().split()) for seg in earlier_segments
):
# Нет более раннего сегмента с mkr/район-словом → это не заявленный
# 3-сегментный district-chain, а голый 2-сегментный адрес — не
# фабрикуем уличный токен, отдаём другим веткам (итог — "", "").
continue
raw_words = segments[street_idx].strip().split()
if not raw_words or len(raw_words) > 3:
continue
if any(

View file

@ -569,6 +569,26 @@ def test_parse_street_house_bare_street_regression_guards() -> None:
assert estimator._parse_street_house("мкр. ЖБИ, улица Сыромолотова") == ("сыромолотова", "")
@pytest.mark.parametrize(
"addr",
[
# Deep-review over-merge risk (#2291): голый 2-сегментный адрес БЕЗ
# какого-либо mkr/район-контекста — частый неформальный ЕКБ-шорткат в
# объявлениях (район/ЖК + номер дома, без слова «улица»/«мкр»/«м-н»).
# Bare-street ветка НЕ должна фабриковать уличный токен из района/ЖК —
# нет более раннего сегмента с mkr-словом → должно остаться ("", "").
"ВИЗ, 24",
"Академический, 24",
"ЖК Некий, 24",
],
)
def test_parse_street_house_bare_street_requires_earlier_mkr_segment(addr: str) -> None:
# Тот же bare-2-сегментный паттерн, что у целевого кейса («<имя>, <номер>»),
# но БЕЗ более раннего mkr-сегмента — ветка должна самоограничиться и
# вернуть безопасный ("", ""), а не сфабрикованный уличный токен.
assert estimator._parse_street_house(addr) == ("", "")
def test_dedup_collapses_domclick_bare_street_mkr_misfire(monkeypatch: pytest.MonkeyPatch) -> None:
# Класс 1 (#2291) e2e: DomClick «Заречный м-н, Готвальда, 24» раньше
# ключевался по району и никогда не сливался с avito/cian, где «готвальда»