Областной ДомКлик отбирается ещё и по координате, не только по префиксу
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m31s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped

Замер на собранном корпусе: у 2 960 из 2 961 карточки областного батча первый
компонент адреса — «Московская область», а у одной «Можайский муниципальный
округ, д. Семёновское, 1» (55.5116/35.8293). Это настоящая область, строгий
префикс выбросил бы её молча.

Фильтр стал payload-уровневым: префикс → да; «Москва» первым компонентом → нет
(раньше гео-ветки, иначе вложенный в областной bbox Москвы протащил бы столицу);
иначе координата внутри bbox_region региона 50 из реестра. Московский батч тем
же замером даёт «Москва» у всех 22 836 карточек — вторая опора его не трогает.
This commit is contained in:
bot-backend 2026-09-12 19:54:33 +03:00
parent a7470f0c3d
commit 02a1ec08f3
2 changed files with 75 additions and 1 deletions

View file

@ -141,6 +141,7 @@ from sqlalchemy.orm import Session
from app.core.db import SessionLocal
from app.services import dadata
from app.services.geocoder import normalize_address
from app.services.regions import REGIONS, is_within_bbox
from app.services.scraper_adapters import RealMatcherAdapter
logger = logging.getLogger(__name__)
@ -606,6 +607,36 @@ def is_oblast_domclick_address(address: str | None) -> bool:
return bool(parts) and parts[0] == "Московская область"
def _payload_point(payload: dict) -> tuple[float, float] | None:
"""(lat, lon) из сырья ДомКлика, если сборщик их положил и они читаются."""
try:
return float(payload["lat"]), float(payload["lon"])
except (KeyError, TypeError, ValueError):
return None
def is_oblast_domclick_payload(payload: dict) -> bool:
"""Область у ДомКлика: префикс адреса ИЛИ координата внутри bbox области.
Одного префикса мало. Замер на собранном корпусе 12.09.2026 (2 961 карточка
областного батча): «Московская область» стоит первым компонентом у 2 960, а
у одной «Можайский муниципальный округ, д. Семёновское, 1», 55.5116/35.8293.
Это настоящая область (Можайск), и строгий префикс выбросил бы её молча.
Московский батч тем же замером даёт «Москва» первым компонентом у ВСЕХ
22 836 карточек, поэтому явный отказ Москве идёт раньше гео-ветки и bbox
Москвы (вложенный в областной) не может протащить столичную карточку в 50.
"""
address = payload.get("address")
if is_oblast_domclick_address(address):
return True
if is_moscow_domclick_address(address):
return False
point = _payload_point(payload)
if point is None:
return False
return is_within_bbox(point[0], point[1], REGIONS[OBLAST_REGION_CODE].bbox_region)
def _by_address(fn: Callable[[str | None], bool]) -> Callable[[dict], bool]:
"""Адаптер: фильтр по адресу → фильтр по всему payload'у (для реестра)."""
return lambda payload: fn(payload.get("address"))
@ -622,7 +653,7 @@ REGION_FILTERS: dict[tuple[str, int], Callable[[dict], bool]] = {
("yandex", MOSCOW_REGION_CODE): _by_address(is_moscow_yandex_address),
("yandex", OBLAST_REGION_CODE): _by_address(is_oblast_yandex_address),
("domclick", MOSCOW_REGION_CODE): _by_address(is_moscow_domclick_address),
("domclick", OBLAST_REGION_CODE): _by_address(is_oblast_domclick_address),
("domclick", OBLAST_REGION_CODE): is_oblast_domclick_payload,
}

View file

@ -491,6 +491,48 @@ def test_domclick_oblast_is_recognised_without_geocode() -> None:
assert not mod.is_oblast_domclick_address(None)
def test_domclick_oblast_without_prefix_is_saved_by_its_coordinates() -> None:
"""Живой случай: областная карточка БЕЗ префикса «Московская область».
В собранном корпусе 12.09.2026 такая ровно одна на 2 961 Можайск, и
строгий префикс выбросил бы её молча. Координата внутри bbox области
вторая опора фильтра, а не замена первой.
"""
mozhaysk = _payload(
source="domclick",
address="Можайский муниципальный округ, д. Семёновское, 1",
lat=55.511625,
lon=35.829308,
)
assert mod.is_oblast_domclick_payload(mozhaysk)
# Строки вместо чисел (JSON сырья бывает и таким) читаются так же.
assert mod.is_oblast_domclick_payload({**mozhaysk, "lat": "55.511625", "lon": "35.829308"})
def test_domclick_oblast_geo_branch_cannot_swallow_moscow() -> None:
"""bbox Москвы вложен в областной — отказ по адресу обязан идти РАНЬШЕ гео."""
moscow = _payload(
source="domclick",
address="Москва, улица Нижние Мнёвники, 16 к2",
lat=55.7558,
lon=37.6173,
)
assert not mod.is_oblast_domclick_payload(moscow)
def test_domclick_oblast_geo_branch_needs_a_usable_point() -> None:
"""Нет префикса и нечего проверить координатой → карточка НЕ область."""
bare = _payload(source="domclick", address="Можайский муниципальный округ, д. Семёновское, 1")
bare.pop("lat", None)
bare.pop("lon", None)
assert not mod.is_oblast_domclick_payload(bare)
assert not mod.is_oblast_domclick_payload({**bare, "lat": None, "lon": None})
assert not mod.is_oblast_domclick_payload({**bare, "lat": "н/д", "lon": "н/д"})
# Координата за пределами области (Екатеринбург) тоже не проходит.
assert not mod.is_oblast_domclick_payload({**bare, "lat": 56.8300, "lon": 60.6000})
def test_domclick_view_is_wired_into_source_views() -> None:
assert mod.SOURCE_VIEWS["domclick"] == "msk_raw.domclick_latest"
filt77 = mod.REGION_FILTERS[("domclick", 77)]
@ -498,6 +540,7 @@ def test_domclick_view_is_wired_into_source_views() -> None:
assert not filt77(_payload(source="domclick", address="Московская область, Химки, 9 Мая, 21к3"))
filt50 = mod.REGION_FILTERS[("domclick", 50)]
assert filt50 is mod.is_oblast_domclick_payload
assert filt50(_payload(source="domclick", address="Московская область, Химки, 9 Мая, 21к3"))
assert not filt50(_payload(source="domclick", address="Москва, улица Нижние Мнёвники, 16 к2"))