gendesign/tradein-mvp/backend/app/api/v1/geocode.py
lekss361 02267d0306 feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in)
- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest

Первый запуск на сервере (вручную, один раз):
  - создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
  - docker network create gendesign_shared (если нет)
  - docker compose -p gendesign-tradein up -d
  - docker compose -p gendesign exec caddy caddy reload
2026-05-21 00:25:39 +03:00

70 lines
2.1 KiB
Python

"""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
from sqlalchemy.orm import Session
from app.core.db import get_db
from app.services.geocoder import GeocodeResult, 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
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,
) -> 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, limit=limit)
return SuggestResponse(items=[
SuggestItem(
label=s.label,
full_address=s.full_address,
lat=s.lat, lon=s.lon, kind=s.kind,
) for s in items
])