fix(tradein): DaData backfill — Екатеринбург city context (#606 hotfix) #607
2 changed files with 127 additions and 2 deletions
|
|
@ -289,10 +289,48 @@ def _update_house_attempt(
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# «Екатеринбург» в любом регистре — короткие адреса типа «улица Малышева, 125»
|
||||||
|
# без явного города DaData по умолчанию маппит в Москву → подсовываем префикс.
|
||||||
|
_EKB_CITY_PREFIX = "Екатеринбург, "
|
||||||
|
_EKB_CITY_MARKER = "екатеринбург"
|
||||||
|
|
||||||
|
|
||||||
|
def _with_city_context(address: str) -> str:
|
||||||
|
"""Pre-pend «Екатеринбург, » если в адресе ещё нет указания города.
|
||||||
|
|
||||||
|
DaData /clean без city-контекста выбирает Москву по умолчанию для
|
||||||
|
тёзок (улица Просторная → ул Просторная в Москве). Этот wrapper гарантирует,
|
||||||
|
что для коротких street-адресов из houses.address DaData получает явный город.
|
||||||
|
|
||||||
|
Идемпотентен: «Екатеринбург, улица Малышева, 125» не префиксируется повторно.
|
||||||
|
"""
|
||||||
|
if not address:
|
||||||
|
return address
|
||||||
|
if _EKB_CITY_MARKER in address.lower():
|
||||||
|
return address
|
||||||
|
return _EKB_CITY_PREFIX + address.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _result_in_ekb(result: DadataAddressResult | None) -> bool:
|
||||||
|
"""Safety: True если DaData вернул действительно Екатеринбургский адрес.
|
||||||
|
|
||||||
|
Защищает от false-positives когда DaData мапит улицу-тёзку (даже после
|
||||||
|
префикса) в другой город. Использует `result.raw.get('city')` (структурное
|
||||||
|
поле) c fallback на substring search в `canonical_address`.
|
||||||
|
"""
|
||||||
|
if result is None:
|
||||||
|
return False
|
||||||
|
city = (result.raw.get("city") or "") if isinstance(result.raw, dict) else ""
|
||||||
|
if _EKB_CITY_MARKER in city.lower():
|
||||||
|
return True
|
||||||
|
canonical = (result.canonical_address or "").lower()
|
||||||
|
return _EKB_CITY_MARKER in canonical
|
||||||
|
|
||||||
|
|
||||||
def _is_enriched_result(result: DadataAddressResult | None) -> bool:
|
def _is_enriched_result(result: DadataAddressResult | None) -> bool:
|
||||||
"""Решает, можно ли overwrite'ить координаты + cadnum по этому result'у.
|
"""Решает, можно ли overwrite'ить координаты + cadnum по этому result'у.
|
||||||
|
|
||||||
Условие: qc_geo IN (0, 1) AND lat / lon заданы.
|
Условие: qc_geo IN (0, 1) AND lat / lon заданы AND city == Екатеринбург.
|
||||||
Возврат False означает «записать только tracking, не overwrite payload».
|
Возврат False означает «записать только tracking, не overwrite payload».
|
||||||
"""
|
"""
|
||||||
if result is None:
|
if result is None:
|
||||||
|
|
@ -301,6 +339,8 @@ def _is_enriched_result(result: DadataAddressResult | None) -> bool:
|
||||||
return False
|
return False
|
||||||
if result.lat is None or result.lon is None:
|
if result.lat is None or result.lon is None:
|
||||||
return False
|
return False
|
||||||
|
if not _result_in_ekb(result):
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -325,7 +365,9 @@ async def _run_backfill(
|
||||||
try:
|
try:
|
||||||
# `clean_address` сам обрабатывает HTTP errors / quota / timeout
|
# `clean_address` сам обрабатывает HTTP errors / quota / timeout
|
||||||
# и возвращает None — exception будет только при unexpected error.
|
# и возвращает None — exception будет только при unexpected error.
|
||||||
result = await clean_address(row.address)
|
# `_with_city_context` префиксирует «Екатеринбург, » если в адресе
|
||||||
|
# ещё нет указания города (защита от Москва-by-default mapping).
|
||||||
|
result = await clean_address(_with_city_context(row.address))
|
||||||
except Exception as exc: # defensive — одна ошибка не должна валить batch
|
except Exception as exc: # defensive — одна ошибка не должна валить batch
|
||||||
stats.failed += 1
|
stats.failed += 1
|
||||||
stats.bump(priority, "failed")
|
stats.bump(priority, "failed")
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,12 @@ from scripts.backfill_houses_dadata import (
|
||||||
HouseRow,
|
HouseRow,
|
||||||
Stats,
|
Stats,
|
||||||
_is_enriched_result,
|
_is_enriched_result,
|
||||||
|
_result_in_ekb,
|
||||||
_run_backfill,
|
_run_backfill,
|
||||||
_select_candidates,
|
_select_candidates,
|
||||||
_update_house_attempt,
|
_update_house_attempt,
|
||||||
_update_house_enriched,
|
_update_house_enriched,
|
||||||
|
_with_city_context,
|
||||||
main,
|
main,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -146,6 +148,87 @@ def test_is_enriched_result_qc_geo_ok_but_no_coords_rejects():
|
||||||
assert _is_enriched_result(res) is False
|
assert _is_enriched_result(res) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_enriched_result_qc_geo_ok_but_non_ekb_city_rejects():
|
||||||
|
"""Safety: qc=0 + coords но город НЕ Екатеринбург → не overwrite.
|
||||||
|
|
||||||
|
DaData без явного города мапит тёзки в Москву (баг dry-run 2026-05-27).
|
||||||
|
Защищаемся post-filter'ом по `canonical_address`/`raw.city`.
|
||||||
|
"""
|
||||||
|
res = _make_dadata_result(
|
||||||
|
qc_geo=0,
|
||||||
|
lat=55.755,
|
||||||
|
lon=37.617,
|
||||||
|
canonical="г Москва, ул Просторная, д 1",
|
||||||
|
)
|
||||||
|
assert _is_enriched_result(res) is False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _with_city_context — pre-pending «Екатеринбург, »
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_city_context_prepends_when_missing():
|
||||||
|
assert _with_city_context("улица Малышева, 125") == "Екатеринбург, улица Малышева, 125"
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_city_context_is_idempotent_when_city_present():
|
||||||
|
"""Уже-полный адрес не префиксируется повторно."""
|
||||||
|
full = "Екатеринбург, улица Малышева, 125"
|
||||||
|
assert _with_city_context(full) == full
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_city_context_case_insensitive():
|
||||||
|
"""ЕКАТЕРИНБУРГ / екатеринбург — тоже не префиксируем."""
|
||||||
|
assert _with_city_context("ЕКАТЕРИНБУРГ, ул. Ленина") == "ЕКАТЕРИНБУРГ, ул. Ленина"
|
||||||
|
assert _with_city_context("екатеринбург, ленина 1") == "екатеринбург, ленина 1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_city_context_empty_passthrough():
|
||||||
|
"""Пустой / None не падает."""
|
||||||
|
assert _with_city_context("") == ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _result_in_ekb — городской post-filter
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_in_ekb_uses_canonical_address_fallback():
|
||||||
|
"""Если raw.city пустой — fallback на substring в canonical_address."""
|
||||||
|
res = _make_dadata_result(canonical="г Екатеринбург, ул Малышева, д 125")
|
||||||
|
assert _result_in_ekb(res) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_in_ekb_uses_raw_city_field():
|
||||||
|
"""raw['city']='Екатеринбург' → True (даже если canonical другое)."""
|
||||||
|
res = _make_dadata_result(canonical="ул Малышева, д 125")
|
||||||
|
res = DadataAddressResult(
|
||||||
|
canonical_address=res.canonical_address,
|
||||||
|
house_cadnum=res.house_cadnum,
|
||||||
|
house_fias_id=res.house_fias_id,
|
||||||
|
lat=res.lat,
|
||||||
|
lon=res.lon,
|
||||||
|
qc_geo=res.qc_geo,
|
||||||
|
qc_house=res.qc_house,
|
||||||
|
kladr_id=None,
|
||||||
|
okato=None,
|
||||||
|
oktmo=None,
|
||||||
|
metro=[],
|
||||||
|
raw={"city": "Екатеринбург"},
|
||||||
|
)
|
||||||
|
assert _result_in_ekb(res) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_in_ekb_rejects_moscow():
|
||||||
|
res = _make_dadata_result(canonical="г Москва, ул Просторная, д 1")
|
||||||
|
assert _result_in_ekb(res) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_in_ekb_none_input():
|
||||||
|
assert _result_in_ekb(None) is False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _update_house_enriched — bind shape
|
# _update_house_enriched — bind shape
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue