"""Geocode endpoints — debug + frontend Suggest proxy.""" from __future__ import annotations import logging from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.core.db import get_db from app.services.geocoder import GeocodeResult, geocode, reverse_geocode, suggest logger = logging.getLogger(__name__) router = APIRouter() @router.get("/lookup", response_model=GeocodeResult) async def lookup( address: Annotated[str, Query(min_length=3, max_length=500)], db: Annotated[Session, Depends(get_db)], ) -> GeocodeResult: """Геокодинг адреса → lat/lon. Примеры: /api/v1/geocode/lookup?address=ул.+Малышева+30+Екатеринбург /api/v1/geocode/lookup?address=Куйбышева+50+Екатеринбург """ result = await geocode(address, db) if result is None: raise HTTPException(status_code=404, detail=f"Address not found: {address}") return result class SuggestItem(BaseModel): label: str full_address: str lat: float lon: float kind: str # ГАР OBJECTGUID (ФИАС) дома — присутствует ТОЛЬКО у house-level кандидатов # DaData-тира. У прочих тиров / street/locality-кандидатов null. Фронт # прокидывает его в POST /estimate как target_fias_id (Tier 0.5 fias_exact). fias_id: str | None = None class SuggestResponse(BaseModel): items: list[SuggestItem] @router.get("/suggest", response_model=SuggestResponse) async def suggest_addresses( q: Annotated[str, Query(min_length=2, max_length=200, description="Запрос для автокомплита")], limit: Annotated[int, Query(ge=1, le=15)] = 8, db: Annotated[Session, Depends(get_db)] = None, # type: ignore[assignment] ) -> SuggestResponse: """Автокомплит адресов в пределах ЕКБ. Используется в EstimateForm для подсказок пока пользователь печатает. Bounded viewbox = ЕКБ (lon 60.40-60.85, lat 56.65-56.95). Пример: /api/v1/geocode/suggest?q=Малышева /api/v1/geocode/suggest?q=Цвиллинга # → пусто, такой улицы в ЕКБ нет """ items = await suggest(q, db=db, limit=limit) return SuggestResponse( items=[ SuggestItem( label=s.label, full_address=s.full_address, lat=s.lat, lon=s.lon, kind=s.kind, fias_id=s.fias_id, ) for s in items ] ) class ReverseResponse(BaseModel): """Reverse-geocode response для MapPicker'а. `lat`/`lon` — echo входной точки клика (для логики «отодвинули ли далеко»). `snapped_lat`/`snapped_lon` — центр matched здания от provider'а. Если `precision in ("exact","number")` фронт двигает marker на snapped point — пользователь видит «магнит к дому». Для остальных precision snapped == input. """ address: str lat: float = Field(..., description="Исходная latitude клика") lon: float = Field(..., description="Исходная longitude клика") snapped_lat: float = Field(..., description="Latitude центра matched здания") snapped_lon: float = Field(..., description="Longitude центра matched здания") precision: str = Field( ..., description=( "Yandex-style: exact/number/street/range/near/locality/other/cadastral. " "Фронт двигает marker только если exact/number/cadastral." ), ) provider: str = Field(..., description="cadastral | yandex | nominatim") @router.get("/reverse", response_model=ReverseResponse) async def reverse( lat: Annotated[float, Query(ge=-90, le=90)], lon: Annotated[float, Query(ge=-180, le=180)], db: Annotated[Session, Depends(get_db)] = None, # type: ignore[assignment] ) -> ReverseResponse: """Обратный геокодинг — координаты с карты → адрес + snapped точка здания. Пример: /api/v1/geocode/reverse?lat=56.8389&lon=60.6057 """ result = await reverse_geocode(lat, lon, db=db) if result is None: raise HTTPException(status_code=404, detail="address not found for coordinates") return ReverseResponse( address=result.address, lat=lat, lon=lon, snapped_lat=result.snapped_lat, snapped_lon=result.snapped_lon, precision=result.precision, provider=result.provider, )