Московский city_hint больше не уходит молча в регион 66 #3470
4 changed files with 104 additions and 8 deletions
|
|
@ -79,7 +79,7 @@ from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.v1.geocode import SuggestResponse, suggest_addresses
|
from app.api.v1.geocode import SuggestResponse, effective_region_code, suggest_addresses
|
||||||
from app.api.v1.trade_in import coverage_probe, estimate
|
from app.api.v1.trade_in import coverage_probe, estimate
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
|
|
@ -222,6 +222,9 @@ class PublicSuggestInput(BaseModel):
|
||||||
q: str = Field(min_length=2, max_length=200)
|
q: str = Field(min_length=2, max_length=200)
|
||||||
limit: int = Field(default=8, ge=1, le=10)
|
limit: int = Field(default=8, ge=1, le=10)
|
||||||
city_hint: str | None = Field(default=None, max_length=100)
|
city_hint: str | None = Field(default=None, max_length=100)
|
||||||
|
# #3051: явный регион с фронта (если он его когда-нибудь пришлёт) —
|
||||||
|
# приоритетнее вывода из city_hint, см. effective_region_code.
|
||||||
|
region_code: int | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
def _fold(text: str) -> str:
|
def _fold(text: str) -> str:
|
||||||
|
|
@ -326,6 +329,7 @@ async def public_suggest(
|
||||||
limit=payload.limit,
|
limit=payload.limit,
|
||||||
db=db,
|
db=db,
|
||||||
city_hint=payload.city_hint,
|
city_hint=payload.city_hint,
|
||||||
|
region_code=effective_region_code(payload.region_code, payload.city_hint),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
_suggest_slots.release()
|
_suggest_slots.release()
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,30 @@ from sqlalchemy.orm import Session
|
||||||
from app.core.db import get_db, run_db_thread
|
from app.core.db import get_db, run_db_thread
|
||||||
from app.services.estimator import _lookup_house_facts
|
from app.services.estimator import _lookup_house_facts
|
||||||
from app.services.geocoder import GeocodeResult, geocode, reverse_geocode, suggest
|
from app.services.geocoder import GeocodeResult, geocode, reverse_geocode, suggest
|
||||||
|
from app.services.regions import DEFAULT_REGION_CODE, region_by_city
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def effective_region_code(region_code: int | None, city_hint: str | None) -> int:
|
||||||
|
"""Регион для геокодера (#3051): явный `region_code` > вывод из `city_hint` > 66.
|
||||||
|
|
||||||
|
До этого хелпера `/suggest` всегда уходил в геокодер с регионом 66
|
||||||
|
по умолчанию — московский `city_hint` («Москва») молча получал
|
||||||
|
свердловский bbox-констрейнт и терял подсказки. Реестр `app.services.regions`
|
||||||
|
уже знает, каким городам какой регион соответствует (REGIONS[77].cities
|
||||||
|
содержит «москва») — используем его вместо повторного захардкоженного списка.
|
||||||
|
"""
|
||||||
|
if region_code is not None:
|
||||||
|
return region_code
|
||||||
|
region = region_by_city(city_hint)
|
||||||
|
if region is not None:
|
||||||
|
return region.code
|
||||||
|
return DEFAULT_REGION_CODE
|
||||||
|
|
||||||
|
|
||||||
@router.get("/lookup", response_model=GeocodeResult)
|
@router.get("/lookup", response_model=GeocodeResult)
|
||||||
async def lookup(
|
async def lookup(
|
||||||
address: Annotated[str, Query(min_length=3, max_length=500)],
|
address: Annotated[str, Query(min_length=3, max_length=500)],
|
||||||
|
|
@ -81,16 +99,17 @@ async def suggest_addresses(
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
region_code: Annotated[
|
region_code: Annotated[
|
||||||
int,
|
int | None,
|
||||||
Query(
|
Query(
|
||||||
description=(
|
description=(
|
||||||
"Регион покрытия (#3051). Дефолт 66 — Свердловская область, прежнее "
|
"Регион покрытия (#3051). None (дефолт) — выводится из `city_hint` "
|
||||||
"поведение для существующих клиентов. 77 — Москва: без него DaData "
|
"через реестр регионов, иначе 66 (Свердловская область, прежнее "
|
||||||
"и Nominatim получают свердловский hard-констрейнт и молча "
|
"поведение). 77 — Москва: без него DaData и Nominatim получают "
|
||||||
"возвращают ПУСТО на московском адресе."
|
"свердловский hard-констрейнт и молча возвращают ПУСТО на "
|
||||||
|
"московском адресе."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] = 66,
|
] = None,
|
||||||
) -> SuggestResponse:
|
) -> SuggestResponse:
|
||||||
"""Автокомплит адресов в регионе `region_code` (дефолт 66 — Свердловская область;
|
"""Автокомплит адресов в регионе `region_code` (дефолт 66 — Свердловская область;
|
||||||
ЕКБ — основной трафик, остаётся быстрым fast-path).
|
ЕКБ — основной трафик, остаётся быстрым fast-path).
|
||||||
|
|
@ -105,8 +124,11 @@ async def suggest_addresses(
|
||||||
/api/v1/geocode/suggest?q=Ленина+1&city_hint=Нижний+Тагил
|
/api/v1/geocode/suggest?q=Ленина+1&city_hint=Нижний+Тагил
|
||||||
/api/v1/geocode/suggest?q=Тверская+6®ion_code=77 # Москва
|
/api/v1/geocode/suggest?q=Тверская+6®ion_code=77 # Москва
|
||||||
"""
|
"""
|
||||||
|
resolved_region_code = effective_region_code(region_code, city_hint)
|
||||||
try:
|
try:
|
||||||
items = await suggest(q, db=db, limit=limit, city_hint=city_hint, region_code=region_code)
|
items = await suggest(
|
||||||
|
q, db=db, limit=limit, city_hint=city_hint, region_code=resolved_region_code
|
||||||
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
# Регион вне реестра покрытия — 422, а не 500: это ошибка ввода клиента.
|
# Регион вне реестра покрытия — 422, а не 500: это ошибка ввода клиента.
|
||||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db"
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from app.api.v1 import geocode as geocode_module
|
||||||
|
from app.api.v1.geocode import SuggestResponse, effective_region_code
|
||||||
from app.services.geocoder import (
|
from app.services.geocoder import (
|
||||||
OBLAST66_VIEWBOX,
|
OBLAST66_VIEWBOX,
|
||||||
SVERDLOVSK_OBLAST_REGION,
|
SVERDLOVSK_OBLAST_REGION,
|
||||||
|
|
@ -179,3 +181,52 @@ async def test_suggest_unknown_region_raises() -> None:
|
||||||
"""Неизвестный регион — ValueError до похода к провайдерам."""
|
"""Неизвестный регион — ValueError до похода к провайдерам."""
|
||||||
with pytest.raises(ValueError, match="unknown region_code"):
|
with pytest.raises(ValueError, match="unknown region_code"):
|
||||||
await suggest("Ленина 1", db=None, limit=5, region_code=99)
|
await suggest("Ленина 1", db=None, limit=5, region_code=99)
|
||||||
|
|
||||||
|
|
||||||
|
# ── API-слой: effective_region_code и хендлер /suggest ──────────────────────
|
||||||
|
#
|
||||||
|
# Всё выше проверяет, что `suggest()` в geocoder.py УМЕЕТ регион. Ниже —
|
||||||
|
# СЛЕДУЮЩИЙ слой той же истории #3051: сам HTTP-хендлер `/suggest` (и его
|
||||||
|
# публичный прокси в mera.py) до сих пор звал `suggest()` с `region_code=66`
|
||||||
|
# по умолчанию БЕЗУСЛОВНО, даже когда `city_hint='Москва'` уже прямо говорил,
|
||||||
|
# какой регион нужен. Приоритет: явный `region_code` > вывод из `city_hint`
|
||||||
|
# (реестр `app.services.regions`) > 66. Инварианты ниже — на хелпере
|
||||||
|
# `effective_region_code`, который эту логику и вносит.
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_region_no_hint_no_explicit_defaults_to_66() -> None:
|
||||||
|
assert effective_region_code(None, None) == 66
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_region_sverdlovsk_city_hint_stays_66() -> None:
|
||||||
|
assert effective_region_code(None, "Серов") == 66
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_region_moscow_city_hint_infers_77() -> None:
|
||||||
|
assert effective_region_code(None, "Москва") == 77
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_region_explicit_wins_over_moscow_hint() -> None:
|
||||||
|
assert effective_region_code(66, "Москва") == 66
|
||||||
|
|
||||||
|
|
||||||
|
async def test_suggest_handler_resolves_region_from_moscow_hint() -> None:
|
||||||
|
"""`suggest_addresses` без явного `region_code` реально передаёт вниз 77,
|
||||||
|
а не только хелпер в изоляции."""
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
async def _fake_suggest(q, db=None, limit=8, city_hint=None, region_code=66):
|
||||||
|
captured["region_code"] = region_code
|
||||||
|
return []
|
||||||
|
|
||||||
|
with patch.object(geocode_module, "suggest", _fake_suggest):
|
||||||
|
result = await geocode_module.suggest_addresses(
|
||||||
|
q="Тверская 6",
|
||||||
|
limit=8,
|
||||||
|
db=MagicMock(),
|
||||||
|
city_hint="Москва",
|
||||||
|
region_code=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, SuggestResponse)
|
||||||
|
assert captured["region_code"] == 77
|
||||||
|
|
|
||||||
|
|
@ -640,6 +640,25 @@ def test_suggest_passes_city_prefixed_query_downstream(client: TestClient) -> No
|
||||||
assert captured["q"] == "Серов, Ленина 1"
|
assert captured["q"] == "Серов, Ленина 1"
|
||||||
# Сам хинт продолжаем передавать: от него зависит гейт кадастрового тира.
|
# Сам хинт продолжаем передавать: от него зависит гейт кадастрового тира.
|
||||||
assert captured["city_hint"] == "Серов"
|
assert captured["city_hint"] == "Серов"
|
||||||
|
# Серов — регион 66, вывод из city_hint не меняет прежнее поведение.
|
||||||
|
assert captured["region_code"] == 66
|
||||||
|
|
||||||
|
|
||||||
|
def test_suggest_infers_region_from_moscow_city_hint(client: TestClient) -> None:
|
||||||
|
"""#3051: `city_hint='Москва'` без явного region_code должен уйти в 77,
|
||||||
|
а не молча получить свердловский region_code=66 по умолчанию.
|
||||||
|
"""
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
async def _fake(**kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
return SuggestResponse(items=[])
|
||||||
|
|
||||||
|
with patch.object(public_mera, "suggest_addresses", _fake):
|
||||||
|
resp = client.post(f"{PREFIX}/suggest", json={"q": "Тверская 6", "city_hint": "Москва"})
|
||||||
|
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
assert captured["region_code"] == 77
|
||||||
|
|
||||||
|
|
||||||
# ── 9. Невалидный вход: 422 и всё тот же бюджет ──────────────────────────────
|
# ── 9. Невалидный вход: 422 и всё тот же бюджет ──────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue