From b7e6aa99aba07295265745d35d440af87e6f6790 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 3 Jul 2026 06:26:12 +0500 Subject: [PATCH] =?UTF-8?q?fix(etl):=20=D0=B3=D0=B5=D0=BE-=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=85=D0=BE=D0=B4=20=E2=80=94=20=D0=BF=D1=80=D0=B5=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=20=C2=AB=D0=B3=20=D0=95=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=BD=D0=B1=D1=83=D1=80=D0=B3=C2=BB=20=D0=B0?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D1=81=D0=B0=D0=BC=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прод dry-run 2026-07-03: confirmed=0/45 — адреса objective_lots идут без города («ул. Гидрострой, д. 2а»), DaData отдаёт qc_geo=5. Объектив = только ЕКБ → безопасный префикс, если города нет в строке. Кэш по исходному адресу. --- .../app/services/etl/objective_backfill.py | 11 ++++++-- .../tests/services/test_objective_backfill.py | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/backend/app/services/etl/objective_backfill.py b/backend/app/services/etl/objective_backfill.py index e678c5d9..1e232317 100644 --- a/backend/app/services/etl/objective_backfill.py +++ b/backend/app/services/etl/objective_backfill.py @@ -841,14 +841,21 @@ def find_geo_matches(db: Session, *, max_distance_m: float = GEO_MAX_DISTANCE_M) geo_cache: dict[str, tuple[float, float] | None] = {} def _geocode(addr: str) -> tuple[float, float] | None: - """Геокод с кэшем + лимитом вызовов. None при отсутствии/лимите.""" + """Геокод с кэшем + лимитом вызовов. None при отсутствии/лимите. + + Адреса objective_lots идут БЕЗ города («ул. Гидрострой, д. 2а») — + DaData на таком отдаёт qc_geo=5 (нет координат): прод-прогон 2026-07-03 + дал confirmed=0/45 именно из-за этого. Объектив покрывает только ЕКБ → + безопасно префиксуем городом, если его нет в строке. + """ if addr in geo_cache: return geo_cache[addr] if report.dadata_calls >= GEO_DADATA_CALL_LIMIT: report.call_limit_hit = True return None + query = addr if "екатеринбург" in addr.lower() else f"г Екатеринбург, {addr}" report.dadata_calls += 1 - coords = clean_address(addr) + coords = clean_address(query) geo_cache[addr] = coords return coords diff --git a/backend/tests/services/test_objective_backfill.py b/backend/tests/services/test_objective_backfill.py index 22551611..17da2198 100644 --- a/backend/tests/services/test_objective_backfill.py +++ b/backend/tests/services/test_objective_backfill.py @@ -598,6 +598,31 @@ def test_geo_ambiguous_resolves_to_nearest() -> None: assert abs(gm.distance_m - 150.0) < 10.0 +def test_geo_geocode_prefixes_city_when_missing() -> None: + """Адреса без города префиксуются «г Екатеринбург, » перед DaData (qc_geo=5 фикс); + адрес с городом уходит как есть.""" + near_lat, near_lon = _offset_coords(_DOMRF_LAT, _DOMRF_LON, 100.0) + calls: list[str] = [] + + mock_db = _make_geo_db( + objective_rows=[("Уникальный", ["Девелопер"], 7001)], + taken_names=[], + domrf_rows=[(301, 'ЖК "Уникальный"', "ООО Другой")], + address_rows=[("Уникальный", "ул. Гидрострой, д. 2а")], + coord_rows=[(301, _DOMRF_LAT, _DOMRF_LON)], + ) + + def _fake_geocode(addr: str) -> tuple[float, float]: + calls.append(addr) + return (near_lat, near_lon) + + with patch(_CLEAN_ADDRESS_PATH, side_effect=_fake_geocode): + report = find_geo_matches(mock_db, max_distance_m=500) + + assert calls == ["г Екатеринбург, ул. Гидрострой, д. 2а"] + assert len(report.confirmed) == 1 + + def test_geo_ambiguous_partial_geocode_does_not_blind_confirm() -> None: """ambiguous, один кандидат геокодится в 100 м, второй НЕ геокодится вовсе → НЕ confirm (reject partial_geocode): «один в радиусе» — артефакт отсутствия