Merge pull request 'подсказки адреса: пустые части адреса выпадают, а не печатаются' (#3276) from fix/mera-suggest-formatting into main
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 16s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been cancelled
Deploy Trade-In / test (push) Has been cancelled

This commit is contained in:
bot-backend 2026-08-30 10:13:01 +00:00
commit c53a967f49
2 changed files with 103 additions and 0 deletions

View file

@ -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 используется

View file

@ -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"