avito_detail.parse_detail_html писал data-map-lat/lon прямо в listings UPDATE COALESCE без bbox-валидации — в отличие от geocoder.py, который фильтрует везде. Итог: 406 active avito-листингов с координатами Питер/Тюмень/Уфа просочились в ЕКБ-датасет (ложные аналоги через Tier A address-match оценщика). Prod-verified 2026-06-23. - geocoder: DRY-хелпер is_within_ekb_bbox(lat, lon, bbox) + именованные EKB_BBOX_TIGHT (geocoder-фильтр, числа не изменены) и EKB_BBOX_WIDE (ingest-guard); 2 inline tight-проверки → хелпер (поведение identical). - avito_detail: после извлечения lat/lon → reset None + logger.warning если вне wide-bbox; листинг падает в geocode-cron путь (COALESCE NULL не затирает прежнее хорошее значение). - tests: bbox-хелпер (ЕКБ True, Питер/Тюмень/Уфа False, inclusive, WIDE⊇TIGHT инвариант) + parse_detail_html guard end-to-end. WIDE строго содержит TIGHT — guard физически не режет ничего, что geocoder сам бы пропустил. NULL lat (14935, geocode backlog) — отдельная проблема, не в scope. One-time cleanup существующих 406 — отдельно (database-expert, prod data mutation). Refs #1871
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
"""Тесты для ЕКБ bbox-хелперов geocoder (#1871).
|
||
|
||
Покрывают:
|
||
- is_within_ekb_bbox_wide — ingest-guard (avito_detail координаты);
|
||
- is_within_ekb_bbox — geocoder tight-фильтр (поведение НЕ изменилось после DRY-рефактора);
|
||
- инвариант WIDE ⊇ TIGHT.
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from app.services.geocoder import (
|
||
EKB_BBOX_TIGHT,
|
||
EKB_BBOX_WIDE,
|
||
is_within_ekb_bbox,
|
||
is_within_ekb_bbox_wide,
|
||
)
|
||
|
||
# Центр ЕКБ (Плотинка) — внутри обоих bbox.
|
||
EKB_CENTER = (56.838, 60.605)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"lat,lon,expected",
|
||
[
|
||
(*EKB_CENTER, True), # центр ЕКБ
|
||
(56.797713, 60.609135, True), # реальная EKB-карточка из avito-fixture
|
||
(59.9386, 30.3141, False), # Питер (#1871)
|
||
(57.1530, 65.5343, False), # Тюмень (#1871)
|
||
(54.7388, 55.9721, False), # Уфа (#1871)
|
||
(57.0, 60.5, True), # приграничье севернее tight, но в wide (В.Пышма зона)
|
||
],
|
||
)
|
||
def test_is_within_ekb_bbox_wide(lat: float, lon: float, expected: bool) -> None:
|
||
assert is_within_ekb_bbox_wide(lat, lon) is expected
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"lat,lon",
|
||
[
|
||
(56.6, 60.3), # юго-западный угол wide
|
||
(57.1, 60.9), # северо-восточный угол wide
|
||
(56.6, 60.9), # юго-восточный угол
|
||
(57.1, 60.3), # северо-западный угол
|
||
],
|
||
)
|
||
def test_wide_bbox_boundary_inclusive(lat: float, lon: float) -> None:
|
||
"""Границы wide-bbox включаются (inclusive)."""
|
||
assert is_within_ekb_bbox_wide(lat, lon) is True
|
||
|
||
|
||
def test_wide_bbox_just_outside() -> None:
|
||
"""Чуть за границей wide-bbox → False."""
|
||
assert is_within_ekb_bbox_wide(56.59, 60.605) is False # южнее
|
||
assert is_within_ekb_bbox_wide(57.11, 60.605) is False # севернее
|
||
assert is_within_ekb_bbox_wide(56.838, 60.29) is False # западнее
|
||
assert is_within_ekb_bbox_wide(56.838, 60.91) is False # восточнее
|
||
|
||
|
||
def test_tight_bbox_behavior_unchanged() -> None:
|
||
"""geocoder tight-фильтр: те же числа, что и до DRY-рефактора (60.40-60.85 / 56.65-56.95)."""
|
||
# внутри
|
||
assert is_within_ekb_bbox(56.838, 60.605) is True
|
||
# границы tight inclusive
|
||
assert is_within_ekb_bbox(56.65, 60.40) is True
|
||
assert is_within_ekb_bbox(56.95, 60.85) is True
|
||
# чуть за tight → False (но в wide)
|
||
assert is_within_ekb_bbox(56.64, 60.605) is False
|
||
assert is_within_ekb_bbox(56.96, 60.605) is False
|
||
assert is_within_ekb_bbox(56.838, 60.39) is False
|
||
assert is_within_ekb_bbox(56.838, 60.86) is False
|
||
|
||
|
||
def test_wide_strictly_contains_tight() -> None:
|
||
"""Инвариант: всё, что прошло бы tight, проходит и wide.
|
||
|
||
Гарантирует, что ingest-guard не режет ничего, что geocoder сам бы пропустил.
|
||
"""
|
||
t_lat_min, t_lat_max, t_lon_min, t_lon_max = EKB_BBOX_TIGHT
|
||
w_lat_min, w_lat_max, w_lon_min, w_lon_max = EKB_BBOX_WIDE
|
||
assert w_lat_min <= t_lat_min
|
||
assert w_lat_max >= t_lat_max
|
||
assert w_lon_min <= t_lon_min
|
||
assert w_lon_max >= t_lon_max
|
||
# точечная проверка на углах tight
|
||
for lat in (t_lat_min, t_lat_max):
|
||
for lon in (t_lon_min, t_lon_max):
|
||
assert is_within_ekb_bbox(lat, lon) is True
|
||
assert is_within_ekb_bbox_wide(lat, lon) is True
|