From 0de77690e996581cc9bd349fad7484c7d013d948 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 4 Jul 2026 15:45:42 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein/estimator):=20=D1=81=D1=83=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20bare-street=20=D0=B2=D0=B5=D1=82=D0=BA?= =?UTF-8?q?=D1=83=20=D0=B4=D0=BE=203-=D1=81=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=D0=B3=D0=BE=20district-chain=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=82=D1=82=D0=B5=D1=80=D0=BD=D0=B0=20(#2291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep-review нашёл over-merge риск: bare-street ветка фабриковала уличный токен на голых 2-сегментных адресах без mkr/район-контекста ("ВИЗ, 24" и т.п.) — теперь требует явный mkr-маркер в более раннем сегменте, ограничивая ветку заявленным 3-сегментным паттерном. --- tradein-mvp/backend/app/services/estimator.py | 24 +++++++++++++++++-- .../test_estimator_dedup_cross_source_2087.py | 20 ++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index aacfbb49..3a2839ca 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -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( diff --git a/tradein-mvp/backend/tests/test_estimator_dedup_cross_source_2087.py b/tradein-mvp/backend/tests/test_estimator_dedup_cross_source_2087.py index 07315039..ba744d3f 100644 --- a/tradein-mvp/backend/tests/test_estimator_dedup_cross_source_2087.py +++ b/tradein-mvp/backend/tests/test_estimator_dedup_cross_source_2087.py @@ -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, где «готвальда»