fix(tradein): подсказки адреса — пустые части адреса выпадают, а не печатаются
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 16s
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 5m50s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 16s
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 5m50s
Прод отдавал «г Екатеринбург, ул Фролова , д. 29, корп.»: реестровый readable_address (ЕГРН) приходит УЖЕ склеенным вместе с пустыми частями — маркер без значения печатается, лишний пробел перед запятой остаётся, и все тиры /suggest (cadastral / geoportal / houses / DaData / Nominatim) пропускали строку насквозь. Чиним склейку, а не конкретный случай: `tidy_address()` — один проход по частям (схлопнуть пробелы, выбросить пустые и маркер-без-номера: корп./стр./лит./кв./оф./пом. и пр.), склейка обратно. Применяется в `GeocodeSuggestion.__post_init__` — единственной точке, через которую проходят все тиры, включая локальные, которые питают и `geocode()`. Тест на функцию склейки, фальсифицирован снятием фильтра пустых: краснеет по значению строки (10 из 14), а не исключением. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
56e4de0b35
commit
663d63aea4
2 changed files with 103 additions and 0 deletions
|
|
@ -810,6 +810,33 @@ async def _nominatim_lookup(address: str, city_hint: str | None = None) -> Geoco
|
|||
# ── Suggest (автокомплит) ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
# Части адреса приходят из реестров УЖЕ склеенными — и склеенными грязно:
|
||||
# ЕГРН `readable_address` печатает маркер без значения и оставляет лишний
|
||||
# пробел («г Екатеринбург, ул Фролова , д. 29, корп.»). Чиним склейку, а не
|
||||
# конкретный случай: один проход по частям — схлопнуть пробелы, выбросить
|
||||
# пустые и маркер-без-номера (корп./стр./лит./кв. и пр.), склеить обратно.
|
||||
_RE_ADDR_WS = re.compile(r"\s+")
|
||||
_RE_ADDR_BARE_MARKER = re.compile(
|
||||
r"^(?:д|дом|корп|корпус|к|стр|строение|соор|сооружение|лит|литера|литер"
|
||||
r"|кв|квартира|оф|офис|пом|помещение|вл|владение|уч|участок)\.?$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def tidy_address(s: str) -> str:
|
||||
"""«г Екатеринбург, ул Фролова , д. 29, корп.» → «г Екатеринбург, ул Фролова, д. 29».
|
||||
|
||||
Часть без значения ВЫПАДАЕТ, а не печатается пустым маркером.
|
||||
"""
|
||||
parts = []
|
||||
for raw in s.split(","):
|
||||
part = _RE_ADDR_WS.sub(" ", raw).strip()
|
||||
if not part or _RE_ADDR_BARE_MARKER.match(part):
|
||||
continue
|
||||
parts.append(part)
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GeocodeSuggestion:
|
||||
label: str # формат "Малышева 30, Октябрьский район"
|
||||
|
|
@ -822,6 +849,12 @@ class GeocodeSuggestion:
|
|||
# street/locality-кандидатов остаётся None (нет стабильного house-fias).
|
||||
fias_id: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Единственная точка, через которую проходят ВСЕ тиры (cadastral/
|
||||
# geoportal/houses/DaData/Nominatim) — чистим здесь, а не у каждого.
|
||||
object.__setattr__(self, "label", tidy_address(self.label))
|
||||
object.__setattr__(self, "full_address", tidy_address(self.full_address))
|
||||
|
||||
|
||||
# ── DaData suggest wrapper ──────────────────────────────────────────────────
|
||||
# Маппинг DaData kind → GeocodeSuggestion.kind (внутри geocoder используется
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
"""Склейка адреса в подсказках: пустые части ВЫПАДАЮТ, а не печатаются.
|
||||
|
||||
Прод-дефект: «г Екатеринбург, ул Фролова , д. 29, корп.» — двойной пробел
|
||||
перед запятой и висячее «корп.» без номера (ЕГРН `readable_address` отдаёт
|
||||
маркер даже когда значения нет, и все тиры /suggest пропускали строку насквозь).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.geocoder import GeocodeSuggestion, tidy_address
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "expected"),
|
||||
[
|
||||
# Прод-случай целиком.
|
||||
(
|
||||
"г Екатеринбург, ул Фролова , д. 29, корп.",
|
||||
"г Екатеринбург, ул Фролова, д. 29",
|
||||
),
|
||||
# Непустой корпус ОБЯЗАН остаться.
|
||||
(
|
||||
"г Екатеринбург, ул Фролова, д. 29, корп. 2",
|
||||
"г Екатеринбург, ул Фролова, д. 29, корп. 2",
|
||||
),
|
||||
# Соседние типы частей — та же болезнь, тот же механизм.
|
||||
("г Екатеринбург, ул Мира, д. 1, стр.", "г Екатеринбург, ул Мира, д. 1"),
|
||||
("г Екатеринбург, ул Мира, д. 1, лит.", "г Екатеринбург, ул Мира, д. 1"),
|
||||
("г Екатеринбург, ул Мира, д. 1, кв.", "г Екатеринбург, ул Мира, д. 1"),
|
||||
("г Екатеринбург, ул Мира, д. 1, стр. 5", "г Екатеринбург, ул Мира, д. 1, стр. 5"),
|
||||
("г Екатеринбург, ул Мира, д. 1, лит. А", "г Екатеринбург, ул Мира, д. 1, лит. А"),
|
||||
# Пустая часть посередине и подряд идущие запятые.
|
||||
("г Екатеринбург, , ул Мира, корп. , д. 1", "г Екатеринбург, ул Мира, д. 1"),
|
||||
# Маркер без точки и без номера.
|
||||
("г Екатеринбург, ул Мира, д 1, корпус", "г Екатеринбург, ул Мира, д 1"),
|
||||
# Чистая строка не меняется.
|
||||
("г Екатеринбург, ул Малышева, д 30", "г Екатеринбург, ул Малышева, д 30"),
|
||||
],
|
||||
)
|
||||
def test_tidy_address(raw: str, expected: str) -> None:
|
||||
assert tidy_address(raw) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw",
|
||||
[
|
||||
"г Екатеринбург, ул Фролова , д. 29, корп.",
|
||||
"г Екатеринбург, , ул Мира, корп. , д. 1",
|
||||
"г Екатеринбург, ул Мира, д. 1, стр. , лит.",
|
||||
],
|
||||
)
|
||||
def test_no_double_spaces_or_dangling_parts(raw: str) -> None:
|
||||
out = tidy_address(raw)
|
||||
assert " " not in out
|
||||
assert " ," not in out
|
||||
assert not out.endswith(",")
|
||||
assert out.split(", ")[-1] not in {"корп.", "стр.", "лит.", "кв.", "д."}
|
||||
|
||||
|
||||
def test_suggestion_cleans_both_label_and_full_address() -> None:
|
||||
"""Чистка стоит в единой точке — её проходят ВСЕ тиры /suggest."""
|
||||
s = GeocodeSuggestion(
|
||||
label="ул Фролова , д. 29, корп.",
|
||||
full_address="г Екатеринбург, ул Фролова , д. 29, корп.",
|
||||
lat=56.8,
|
||||
lon=60.6,
|
||||
kind="house",
|
||||
)
|
||||
assert s.label == "ул Фролова, д. 29"
|
||||
assert s.full_address == "г Екатеринбург, ул Фролова, д. 29"
|
||||
Loading…
Add table
Reference in a new issue