fix(tradein): extract_street_name handles Nominatim reverse format #557
2 changed files with 83 additions and 29 deletions
|
|
@ -1112,49 +1112,70 @@ def _extract_short_addr(full_address: str | None) -> str | None:
|
||||||
return s.strip(" ,.") or None
|
return s.strip(" ,.") or None
|
||||||
|
|
||||||
|
|
||||||
# Strips trailing house-number and any subsequent parts from a short address.
|
# Ищет keyword типа улицы (ул./улица/пр./проспект/...) в адресе.
|
||||||
_TRAILING_HOUSE_RE = re.compile(
|
# Работает для FORWARD и REVERSE форматов Nominatim.
|
||||||
r",\s*\d+.*$",
|
_STREET_KW_RE = re.compile(
|
||||||
flags=re.UNICODE,
|
r"(?<![А-Яа-яёЁa-zA-Z])"
|
||||||
|
r"(?:ул\.|улица|пр\.|пр-т|проспект|пер\.|переулок|"
|
||||||
|
r"б-р|бульвар|ш\.|шоссе|наб\.|набережная|проезд|тракт|"
|
||||||
|
r"пл\.|площадь|мкр\.|мкр|микрорайон)"
|
||||||
|
r"\s+",
|
||||||
|
flags=re.IGNORECASE | re.UNICODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Strips leading street-type prefix (ул., проспект, пер., etc.) from a street token.
|
# После keyword: 1-3 слова имени улицы со стопом на запятую или номер дома.
|
||||||
_STREET_PREFIX_RE = re.compile(
|
# Поддерживает "8 Марта" (цифра + слово) и "Большая Конюшенная" (несколько слов).
|
||||||
r"^(?:ул\.|улица|пр\.|пр-т|проспект|пер\.|переулок|"
|
_STREET_NAME_RE = re.compile(
|
||||||
r"б-р|бульвар|ш\.|шоссе|наб\.|набережная|проезд|тракт|пл\.|площадь|"
|
r"^([0-9]+\s+[А-Яа-яёЁ][А-Яа-яёЁ-]+"
|
||||||
r"мкр\.?|микрорайон)\s+",
|
r"|[А-Яа-яёЁ][А-Яа-яёЁ-]+(?:\s+[А-Яа-яёЁ][А-Яа-яёЁ-]+){0,2})"
|
||||||
flags=re.IGNORECASE | re.UNICODE,
|
r"(?=,|\s+(?:д\.?\s*)?\d|\s*$)",
|
||||||
|
flags=re.UNICODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def extract_street_name(full_address: str | None) -> str | None:
|
def extract_street_name(full_address: str | None) -> str | None:
|
||||||
"""Извлекает только название улицы (без типа и номера дома) из полного адреса.
|
"""Извлекает чистое имя улицы из адреса в FORWARD или REVERSE формате.
|
||||||
|
|
||||||
Используется для запросов в open dataset Росреестра, где адреса агрегированы
|
|
||||||
до уровня улицы (без номера дома).
|
|
||||||
|
|
||||||
Примеры:
|
Примеры:
|
||||||
"Свердловская обл., г. Екатеринбург, ул. Космонавтов, 12" → "Космонавтов"
|
"Екатеринбург, ул. Космонавтов, 50" → "Космонавтов"
|
||||||
"Екатеринбург, проспект Ленина, 50" → "Ленина"
|
"80, улица 8 Марта, Артек, ..., Россия" → "8 Марта"
|
||||||
"Екатеринбург, ул. 8 Марта, 18" → "8 Марта"
|
"проспект Ленина 50" → "Ленина"
|
||||||
"г. Екатеринбург, пер. Красный, 4" → "Красный"
|
"Россия, Екатеринбург, ул. Малышева, 1" → "Малышева"
|
||||||
|
"ул. Большая Конюшенная, 25" → "Большая Конюшенная"
|
||||||
|
"" → None
|
||||||
|
|
||||||
Алгоритм:
|
Алгоритм:
|
||||||
1. _extract_short_addr → «ул. Космонавтов, 12» (или fallback строка).
|
1. Ищем street-keyword (ул/улица/пр/проспект/...) — case-insensitive.
|
||||||
2. Отрезаем trailing «, 12...» (house number и всё после).
|
2. После keyword берём 1-3 слова до запятой или номера дома.
|
||||||
3. Отрезаем leading street-type prefix («ул.», «проспект» и т.д.).
|
3. Если keyword не нашёлся — пытаемся первый capitalized токен с
|
||||||
4. None если ничего не осталось.
|
поиском до запятой или номера (fallback для адресов без keyword'а).
|
||||||
|
|
||||||
|
Returns None если ничего не извлеклось.
|
||||||
"""
|
"""
|
||||||
short = _extract_short_addr(full_address)
|
if not full_address or not full_address.strip():
|
||||||
if not short:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Strip trailing house number: ", 12" / ", 12А" / ", 48/2"
|
s = full_address.strip()
|
||||||
without_house = _TRAILING_HOUSE_RE.sub("", short).strip(" ,.")
|
|
||||||
|
|
||||||
# Strip leading type prefix
|
# 1. Keyword-based extraction (работает для обоих форматов: forward и reverse)
|
||||||
street_name = _STREET_PREFIX_RE.sub("", without_house).strip()
|
m = _STREET_KW_RE.search(s)
|
||||||
return street_name or None
|
if m:
|
||||||
|
rest = s[m.end():].lstrip()
|
||||||
|
nm = _STREET_NAME_RE.match(rest)
|
||||||
|
if nm:
|
||||||
|
return nm.group(1).strip()
|
||||||
|
|
||||||
|
# 2. Fallback: нет keyword — пробуем первый capitalized токен
|
||||||
|
# Используется для "Большая Конюшенная, 25" без "ул."
|
||||||
|
nm = _STREET_NAME_RE.match(s)
|
||||||
|
if nm:
|
||||||
|
candidate = nm.group(1).strip()
|
||||||
|
# Отсеиваем очевидные административные слова
|
||||||
|
bad = {"Россия", "Москва", "Санкт-Петербург", "область", "район", "округ", "край"}
|
||||||
|
if not any(b in candidate for b in bad):
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _stratify_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _stratify_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,39 @@ def test_extract_street_name_no_street_keyword() -> None:
|
||||||
assert result is None or isinstance(result, str)
|
assert result is None or isinstance(result, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"address,expected",
|
||||||
|
[
|
||||||
|
# Forward format (user-typed)
|
||||||
|
("Екатеринбург, ул. Космонавтов, 50", "Космонавтов"),
|
||||||
|
("Россия, Екатеринбург, ул. Малышева, 1", "Малышева"),
|
||||||
|
("проспект Ленина 50", "Ленина"),
|
||||||
|
("ул. Большая Конюшенная, 25", "Большая Конюшенная"),
|
||||||
|
# Reverse format (from Nominatim geocoder)
|
||||||
|
(
|
||||||
|
"80, улица 8 Марта, Артек, Форум-Сити, Ленинский район, Екатеринбург,"
|
||||||
|
" городской округ Екатеринбург, Свердловская область,"
|
||||||
|
" Уральский федеральный округ, 640144, Россия",
|
||||||
|
"8 Марта",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"50, улица Космонавтов, Орджоникидзевский район, Екатеринбург, Россия",
|
||||||
|
"Космонавтов",
|
||||||
|
),
|
||||||
|
# Edge cases
|
||||||
|
("", None),
|
||||||
|
(None, None),
|
||||||
|
("just plain text", None),
|
||||||
|
("Москва", None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_extract_street_name_parametrized(address: str | None, expected: str | None) -> None:
|
||||||
|
"""Parametrized cases covering forward, reverse (Nominatim), and edge cases."""
|
||||||
|
from app.services.estimator import extract_street_name
|
||||||
|
|
||||||
|
assert extract_street_name(address) == expected
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue