Строгое region_code = CAST(:region_code AS integer), без OR-ветки на NULL.
Второй прод-замер (13.09.2026, полный набор фильтров модуля — is_active,
price sanity, geo_precision, свежесть 14 дней, сегмент, bbox ядра 66):
15 858 строк проходят фильтры, из них 3 с NULL region_code. Медиана с ними
151 101 ₽/м², без них 151 103 — сдвиг 2 рубля на 151 тысяче (0.001%),
для региона 66 статистически ничто.
Мягкий OR NULL был бы лазейкой того же класса, что и сам дефект: в
listings 16 930 NULL-region строк Avito, 24 из них уже лежат в bbox
Москвы (сегодня не проходят по свежести, но могут завтра) — OR NULL
тихо открыл бы им дорогу обратно в московскую медиану тем же путём,
каким туда раньше попадала Московская область.
Тесты: test_median_queries_do_not_admit_null_region_code (обе median-SQL
не содержат "OR region_code" / "region_code IS NULL"), комментарий перед
SQL переписан под новое решение с этими числами.
Дефект 1: bbox продукт-ядра Москвы (55.55-55.95/37.30-37.90) — прямоугольник,
не административная граница, и захватывает соседние Химки/Реутов/Котельники/
Люберцы (region_code=50). Обе SQL-медианы (_MEDIAN_PPM2_LOCAL_SQL/CITYWIDE_SQL)
фильтровали листинги только по bbox, без region_code — прод-замер 13.09.2026:
городская медиана Москвы получалась заниженной на 7.9% (422 222 вместо 458 208
₽/м²) из-за примеси 16 801 подмосковного лота. У областных строк city IS NULL,
поэтому фильтровать по имени города нельзя — добавлен bind-параметр
region_code, заполняемый из уже определённого для точки Region.code.
Для 66 сохранено byte-for-byte: `OR region_code IS NULL` оставляет в пуле
некоторое количество legacy-листингов без региональной метки — ровно то же
множество строк, что и старый запрос без фильтра вовсе (проверено на проде:
внутри bbox 66 других region_code, кроме 66 и NULL, не существует).
Дефект 2: гейт _fetch_nearby_poi решал poi_status по count(*) ПО ВСЕЙ
osm_poi_ekb_local (4 850 екатеринбургских строк, всегда > 0) — для Москвы
это давало poi_status="ok" с пустым списком «что рядом», хотя внутри
московского продукт-ядра POI нет вообще. Гейт теперь считает count в
bbox_product_core региона точки; при 0 — тот же статус "unavailable",
что и раньше (терминология не расширяется, trade_in.py трактует его как
«нет данных» без изменений).
Тесты: tests/services/test_location_index.py — добавлены проверки bind-
параметра region_code в обеих median-SQL, региональной изоляции gate'а POI
(bbox запроса совпадает с bbox_product_core переданного региона), резолва
московской точки в region 77, и end-to-end сценария для Москвы (region_code
долетает в запрос, poi_status="unavailable"). 43/43 pytest зелёные,
ruff check + ruff format --check чисто.