fix(tradein/estimator): дом «2» вместо «15» при глухом названии станции метро в avito-адресе (#2429)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 45s
Deploy Trade-In / build-backend (push) Successful in 55s
Deploy Trade-In / deploy (push) Successful in 46s
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 45s
Deploy Trade-In / build-backend (push) Successful in 55s
Deploy Trade-In / deploy (push) Successful in 46s
This commit is contained in:
parent
cfed042712
commit
752c3656a3
2 changed files with 159 additions and 3 deletions
|
|
@ -4954,8 +4954,19 @@ _DEDUP_MKR_SUFFIX_RE = re.compile(
|
|||
# Номер дома = первое число ПОСЛЕ названия улицы (опц. «д.»/«дом»), с литерой/
|
||||
# корпусом («46», «8а», «12/3»). Ищется в остатке строки от конца street-матча —
|
||||
# ведущая цифра нумерованной улицы («8 Марта») сюда не попадает.
|
||||
# #2423: суффикс-буква («46г», «8а») засчитывается только если ЗА НЕЙ не идёт
|
||||
# ЕЩЁ ОДИН word-символ (`(?!\w)` — буква ИЛИ цифра), т.е. только если это
|
||||
# последний word-символ токена. Иначе это не литера дома, а: (а) первая буква
|
||||
# приклеенного без пробела слова (название станции метро в avito:
|
||||
# «2Чкаловская11-15 мин» → дом «2», а не «15» из хвоста дистанции), либо
|
||||
# (б) буква корпуса, приклеенная без пробела к своей цифре («6к1» → дом «6»,
|
||||
# ту же цифру даёт и вариант с пробелом «6 к1» — иначе кросс-source guard
|
||||
# ошибочно видел бы «6к» vs «6» как разные дома и блокировал слияние).
|
||||
# Trailing `\b` намеренно убран: он ложно "отменял" весь матч цифры, когда
|
||||
# сразу за ней шло кириллическое слово без разделителя (Cyrillic letter и
|
||||
# digit — оба `\w`, между ними нет word-boundary).
|
||||
_DEDUP_HOUSE_NO_RE = re.compile(
|
||||
r"\b(?:д(?:ом)?\.?\s*)?(\d+(?:[а-яё]|/\d+)?)\b",
|
||||
r"\b(?:д(?:ом)?\.?\s*)?(\d+(?:[а-яё](?!\w)|/\d+)?)",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
# Слова-маркеры дома, которые могли попасть в захват названия (напр. «ленина д 5»).
|
||||
|
|
|
|||
|
|
@ -411,7 +411,12 @@ def test_dedup_empty_cadnum_and_street_stay_separate(monkeypatch: pytest.MonkeyP
|
|||
[
|
||||
# --- контрольные (существующая ветка «тип ПЕРЕД именем») — БАЙТ-В-БАЙТ
|
||||
# тот же токен, что дают новые ветки, иначе клейка развалится.
|
||||
("улица Рассветная, 6к1", ("рассветная", "")), # cian-стиль, house не извлекается
|
||||
# #2423: «6к1» (cian-стиль, литера корпуса приклеена без пробела к
|
||||
# цифре) теперь даёт house="6", а не "" — та же цифра, что и у
|
||||
# пробельного «6 к1» ниже (домклик-стиль), т.е. извлечение стало
|
||||
# согласованным между форматами (было: полный отказ матча из-за
|
||||
# word-boundary collapse между цифрой и кириллической буквой).
|
||||
("улица Рассветная, 6к1", ("рассветная", "6")),
|
||||
("ул. Белинского 86", ("белинского", "86")),
|
||||
("улица 8 Марта, 46", ("8 марта", "46")), # нумерованная, ведущая цифра в токене
|
||||
# --- (а) имя ПЕРЕД типом (формат domklik)
|
||||
|
|
@ -420,7 +425,8 @@ def test_dedup_empty_cadnum_and_street_stay_separate(monkeypatch: pytest.MonkeyP
|
|||
("Красных Командиров улица, 10", ("красных командиров", "10")),
|
||||
("8 Марта улица, 46", ("8 марта", "46")), # нумерованная, имя-перед-типом
|
||||
# --- (б) тип-суффикс приклеен запятой/точкой без пробела (avito)
|
||||
("Рассветная ул.,6к1", ("рассветная", "")),
|
||||
# #2423: аналогично, house="6" вместо "" (см. комментарий выше).
|
||||
("Рассветная ул.,6к1", ("рассветная", "6")),
|
||||
("Белинского ул., 86", ("белинского", "86")),
|
||||
# --- (в) fallback без уличного типа — микрорайон, префикс «mkr »
|
||||
("Екатеринбург, Светлый микрорайон, 10", ("mkr светлый", "10")),
|
||||
|
|
@ -637,3 +643,142 @@ def test_dedup_collapses_yo_ye_variant(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
]
|
||||
out = estimator._dedup_cross_source(lots)
|
||||
assert len(out) == 1
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# #2423 avito glued-metro-name house-number extraction
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_parse_street_house_avito_glued_metro_suffix() -> None:
|
||||
# Root cause прод-кейса: станция метро приклеена к номеру дома без
|
||||
# разделителя («2Чкаловская»). Trailing `\b` в старом regex ложно отменял
|
||||
# матч цифры целиком (Cyrillic-буква и цифра — оба `\w`, границы нет), и
|
||||
# .search() уезжал вправо до «11-15» (дистанция до метро), где дефис даёт
|
||||
# настоящую границу → дом ошибочно распознавался как «15» вместо «2».
|
||||
addr = "ул. Амундсена, 2Чкаловская11-15 мин. пешком"
|
||||
assert estimator._parse_street_house(addr) == ("амундсена", "2")
|
||||
|
||||
|
||||
def test_parse_street_house_avito_glued_metro_hyphenated_name() -> None:
|
||||
# Приклеенное название станции само содержит дефис — фикс не должен
|
||||
# зависеть от того, есть ли внутри глухого слова дефис или нет.
|
||||
addr = "ул. Космонавтов, 14Ботаническая-Уктус7-12 мин. пешком"
|
||||
assert estimator._parse_street_house(addr) == ("космонавтов", "14")
|
||||
|
||||
|
||||
def test_parse_street_house_avito_glued_metro_short_name() -> None:
|
||||
# Адверсариальный минимум: приклеенное слово всего 2 буквы — lookahead
|
||||
# должен отклонить суффикс-букву даже на короткой глухой строке, не
|
||||
# только на длинных названиях станций.
|
||||
addr = "ул. Мира, 9ВЗ5-8 мин. пешком"
|
||||
assert estimator._parse_street_house(addr) == ("мира", "9")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("house_part", "expected_house"),
|
||||
[
|
||||
("24г", "24г"),
|
||||
("32а", "32а"),
|
||||
("5б", "5б"),
|
||||
],
|
||||
)
|
||||
def test_parse_street_house_letter_suffix_not_regressed(
|
||||
house_part: str, expected_house: str
|
||||
) -> None:
|
||||
# Настоящая литера дома (не продолжается ещё одной кириллической буквой,
|
||||
# т.е. это последний word-символ токена) — поведение не должно измениться.
|
||||
addr = f"ул. Ленина, {house_part}"
|
||||
assert estimator._parse_street_house(addr) == ("ленина", expected_house)
|
||||
|
||||
|
||||
def test_parse_street_house_corpus_slash_not_regressed() -> None:
|
||||
# «12/3» (корпус через слэш) — вторая альтернатива (`/\d+`) не тронута фиксом.
|
||||
assert estimator._parse_street_house("ул. Ленина, 12/3") == ("ленина", "12/3")
|
||||
|
||||
|
||||
def test_parse_street_house_glued_corpus_letter_partial_capture() -> None:
|
||||
# #2423: «10к1» без пробела перед литерой корпуса — литера «к» отклоняется
|
||||
# как суффикс, т.к. за ней идёт ЕЩЁ ОДИН word-символ (цифра «1» корпуса),
|
||||
# `(?!\w)` (не только `(?![а-яё])` из первоначального черновика фикса)
|
||||
# отсекает и этот случай. Даёт «10» — тот же номер дома, что и у
|
||||
# пробельного варианта «10 к1» (см. регрессию ниже) → house-guard видит
|
||||
# одинаковый номер дома в обоих написаниях, кросс-source merge не ломается.
|
||||
# ВАЖНО: черновой вариант фикса с `(?![а-яё])` (отклонять суффикс только
|
||||
# если за ним ЕЩЁ ОДНА БУКВА) давал «10к» здесь — рассинхронизация с
|
||||
# пробельным вариантом «6 к1» → «6» сломала 2 существующих e2e-теста
|
||||
# (test_dedup_collapses_domklik_cian_name_order,
|
||||
# test_dedup_collapses_domklik_cian_area_straddle), т.к. guard_house видел
|
||||
# «6к» != «6» и блокировал слияние. Обнаружено прогоном полного набора
|
||||
# тестов при реализации — `(?!\w)` устраняет регрессию.
|
||||
assert estimator._parse_street_house("ул. Ленина, 10к1") == ("ленина", "10")
|
||||
|
||||
|
||||
def test_parse_street_house_ambiguous_glued_digit_runs_limitation() -> None:
|
||||
# #2423 limitation: два приклеенных друг к другу чисто-числовых прогона
|
||||
# без буквы/пунктуации между ними принципиально неразличимы регэкспом —
|
||||
# какое число дом, а какое дистанция, знает только семантика. В реальных
|
||||
# avito-адресах между домом и дистанцией всегда есть «мин»/имя станции
|
||||
# (см. тесты выше), так что этот кейс не встречается в проде; закрепляем
|
||||
# текущий (не «исправленный») результат, чтобы будущее изменение было
|
||||
# осознанным видимым диффом, а не тихой регрессией.
|
||||
assert estimator._parse_street_house("ул. Ленина, 7500 м до метро") == ("ленина", "7500")
|
||||
|
||||
|
||||
def test_dedup_collapses_avito_glued_metro_suffix_house_match(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# e2e: avito-лот с приклеенным названием станции (дом «2» до фикса терялся,
|
||||
# извлекалось «15») + чистый cian-адрес того же дома «2» → house-guard
|
||||
# больше не блокирует street-union (оба извлекают «2») → один физлот.
|
||||
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", True)
|
||||
lots = [
|
||||
_lot(
|
||||
source="avito",
|
||||
address="ул. Амундсена, 2Чкаловская11-15 мин. пешком",
|
||||
area=45.0,
|
||||
price=6_100_000,
|
||||
floor=5,
|
||||
cad=None,
|
||||
),
|
||||
_lot(
|
||||
source="cian",
|
||||
address="ул. Амундсена, 2",
|
||||
area=45.0,
|
||||
price=6_100_000,
|
||||
floor=5,
|
||||
cad=None,
|
||||
),
|
||||
]
|
||||
out = estimator._dedup_cross_source(lots)
|
||||
assert len(out) == 1
|
||||
|
||||
|
||||
def test_dedup_glued_metro_noise_does_not_cause_over_merge(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Анти-регрессия: фикс не должен сделать guard_house снисходительнее для
|
||||
# адресов с глухим метро-шумом в общем случае — ДЕЙСТВИТЕЛЬНО разные дома
|
||||
# («24» vs «26»), каждый одет в приклеенное название станции разной длины,
|
||||
# ДОЛЖНЫ остаться раздельными (guard всё ещё блокирует both-present-different).
|
||||
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", True)
|
||||
lots = [
|
||||
_lot(
|
||||
source="avito",
|
||||
address="ул. Гагарина, 24Чкаловская5-8 мин. пешком",
|
||||
area=45.0,
|
||||
price=6_100_000,
|
||||
floor=5,
|
||||
cad=None,
|
||||
),
|
||||
_lot(
|
||||
source="cian",
|
||||
address="ул. Гагарина, 26Ботаническая11-15 мин. пешком",
|
||||
area=45.0,
|
||||
price=6_100_000,
|
||||
floor=5,
|
||||
cad=None,
|
||||
),
|
||||
]
|
||||
out = estimator._dedup_cross_source(lots)
|
||||
assert len(out) == 2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue