"""Геокодирование: прокси к Nominatim (OpenStreetMap). GET /api/v1/geocode?q=<запрос>&limit=5 Возвращает {results: [{lat, lng, display_name, type}]}. Nominatim Usage Policy: User-Agent обязателен, ≤1 rps — проксируем через бэкенд с корректным UA и ограничением countrycodes=by. Правила запросов (nominatim-грабли, см. homelab-ops skill): - Короткий запрос: пункт + адрес, БЕЗ района/области. - Пункт ПЕРВЫМ словом. """ import os import logging from typing import List, Optional import httpx from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from backend.logging_setup import get_logger from backend.routers.auth import require_roles from fastapi import Depends log = get_logger('geocode') router = APIRouter(prefix='/api/v1/geocode', tags=['geocode']) NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search' USER_AGENT = os.getenv('NOMINATIM_USER_AGENT', 'Vector-MChS/1.0 (geocoding proxy)') COUNTRY_CODE = 'by' class GeocodeResult(BaseModel): lat: float lng: float display_name: str type: Optional[str] = None class GeocodeResponse(BaseModel): results: List[GeocodeResult] @router.get('', response_model=GeocodeResponse, dependencies=[Depends(require_roles(['operator', 'field', 'admin']))]) async def geocode( q: str = Query(..., min_length=2, description='Адрес: «пункт, улица»'), limit: int = Query(5, ge=1, le=10), ): """Геокодирование через Nominatim, ограничено Беларусью.""" headers = {'User-Agent': USER_AGENT} params = { 'q': q.strip(), 'format': 'jsonv2', 'limit': limit, 'countrycodes': COUNTRY_CODE, 'addressdetails': 1, } try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.get(NOMINATIM_URL, headers=headers, params=params) resp.raise_for_status() data = resp.json() except httpx.TimeoutException: log.warning('geocode timeout q=%s', q.strip()[:100]) raise HTTPException(status_code=504, detail='Сервис геокодирования не ответил') except Exception as e: log.warning('geocode error q=%s: %s', q.strip()[:100], e) raise HTTPException(status_code=502, detail=f'Ошибка геокодирования: {e}') log.info('geocode q=%s результатов=%d', q.strip()[:100], len(data)) return {'results': [ GeocodeResult( lat=float(item['lat']), lng=float(item['lon']), display_name=item.get('display_name', ''), type=item.get('type'), ) for item in data ]}