1. Рекомендации не предписывают перекрывать водоёмы/ж/д — оператор ПРОВЕРЯЕТ
их (правки в rules_analysis, scoring_service ×2 копии, water.py + доки).
2. Геокодирование: район нужен (дубли улиц между районами) — подсказка формата
«пункт, район, адрес» и текст ошибки.
198 passed.
При анализе по case_id координаты карточки (tnp_lat/tnp_lon) не попадали
в SearchInput (читались только payload-lat/lon) → rules_analyze не видел
lat/lon и отдавал базовые fallback-зоны «Ближняя N / Средняя E» —
постоянные направления вверх-вправо на карте независимо от местности.
Фикс: case_id-режим подтягивает lat/lon из tnp_* (gps_* как алиас).
198 passed.
- Step3Circumstances: удалено дублирующее «Последнее известное место»
(last_known_place); остаётся только tnp_address на шаге координат.
- Step4Environment: кнопка «Найти координаты» теперь РАБОЧАЯ — вызывает
новый GET /api/v1/geocode (прокси Nominatim, countrycodes=by, короткий
запрос «пункт, адрес» по граблям geocoding), заполняет lat/lon,
показывает найденный display_name или ошибку. Без debounce-автоплейса
(Pitfall 2) — только явный клик.
- Step3Circumstances: «Прошло времени» считается автоматически из
lost_time (пересчёт каждые 30 с, округление до 0.1 ч); поле остаётся
редактируемым для корректировки.
- БАГФИКС: форма шлёт lost_time, модель хранит loss_time — алиас в
_normalize_case_data. Раньше время пропажи молча терялось
(Pydantic игнорирует неизвестные поля) — elapsed_hours приходилось
вводить руками.
- last_known_place убран из валидации шага 4.
- backend/routers/geocode.py: Nominatim-прокси с auth (operator/field/admin).
192 passed, JSX валиден.
Второй баг той же семьи: polygons.append(poly['coordinates'][0]) клал ring
на уровень полигона — у relation с ОДНИМ outer member'ом coordinates
получались depth-2 ([[pair],...]) при type=Polygon. Это 5 крупнейших
водных объектов (Камсамольскае возера и др.) — фронтовый toLatLngs
строил [[undefined,undefined]...] → Leaflet 'latlngs[0] undefined' →
белый экран страницы анализа.
Теперь relation собирает список Polygon'ов (coordinates=[ring]) и:
1 кольцо → отдаётся как Polygon; 2+ → MultiPolygon из polygon.coordinates.
Баг: для relation с 2+ внешними кольцами coordinates отдавался [ring0, ring1]
(глубина 3) при type=MultiPolygon — невалидный GeoJSON, Leaflet падал
('latlngs[0] undefined') → белый экран страницы анализа на кейсах с
мультиполигонами (РАС-кейс Минск: Камсамольскае возера из 2 колец).
Фронт: toLatLngs различает Polygon/MultiPolygon по глубине coords[0][0][0].
Определение специфических рекомендаций (матрица профилей §8):
1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
- /api/v1/water/{case_id} отдаёт railway=rail как LineString
(без service/industrial/military веток), кэш общий v2;
- railway_warning «перекрыть/проверить немедленно» по профилям;
- SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.
2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
- раньше чекбокс влиял только на текст, в скоринге был пробел;
- derive в analyze._derive_profiles — работает и для closed_cases.
3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
«вне поведенческой модели» с пояснением (vector_tasks B12:
профили без аналога не выдавать за учтённые); блок на фронте
в карточке здоровья.
Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
- GET /api/v1/water/{case_id}: полигоны водоёмов/болот OSM вокруг ТНП
(Overpass, natural=water+wetland, кэш 72ч, circuit breaker от geo_service);
бейдж-предупреждение по профилям случая (РАС/эпилепсия/не умеет плавать)
- geo_service: цепочка зеркал Overpass (maps.mail.ru → overpass-api.de):
основной сервер имеет AAAA, на хостах без IPv6-маршрута httpx падал
ConnectError'ом до IPv4-фолбэка; на CT108 фапало стабильно
- SearchMap: слой водоёмов под зонами (синие полигоны, болота пунктиром),
попапы с площадью/дистанцией от ТНП; AnalysisResult грузит слой,
показывает предупреждение профиля и счётчик водоёмов
- проверено на CT108: РАС-кейс (Минск) — 43 объекта, витя-кейс
(Каменец-борисовская обл.) — 7 объектов, включая водохранилище Загацце
Бэкенд:
- schemas.py: ClosedCaseCreate/Response — форма ~25 полей (субъект, среда,
ресурсы, исход) для внесения завершённых поисков вручную
- routers/closed_cases.py: CRUD + POST {id}/reanalyze. _run_model() —
тот же скоринг, что /analyze (WeightedScorer + distance_service +
психотип), НО БЕЗ claude_service: urgency/behavior/actions по правилам
- _accuracy_note(): прогнозный радиус vs факт (в радиусе / мимо)
- main.py: роутер подключён
Фронтенд:
- pages/ClosedCases.jsx + .css: форма внесения (VCL-density, чипы диагнозов/
рельефа), блок 'Прогноз vs факт', таблица архива (в радиусе/мимо)
- AdminDashboard.jsx: вкладки Кейсы | Завершённые поиски
Тесты: test_closed_cases.py — 6 passed. Существующие 15 failed
(test_distance_service) не связаны с изменениями — падали до них.
_extract_json_payload handles a json/JSON/bare fence, raw JSON and JSON
embedded in prose; any unparseable response or contract violation now
degrades to the deterministic scoring_service instead of raising.
Also guards the response envelope itself (content[0].text).
B3: single home for recommendation scoring
services/recommendation_service.py holds the rules; routers/stats.py and
backend/services/stats_service.py both delegate to it. Unified rules are the
union of the two old copies: same weights/threshold, substring matching
(superset of the old exact match), tolerant key aliases, health_flags rule
kept. Endpoint response contract unchanged.
Plus: Overpass circuit breaker and concurrent zone queries in geo_service -
128 sequential calls per analysis no longer each burn a connect timeout when
the host has no outbound network.
Tests: 152 -> 194 passed.