import { NextRequest, NextResponse } from "next/server"; import { sql } from "@/lib/db"; import { cacheGet, cacheSet, hashParams } from "@/lib/cache"; import { CATEGORY_IDS } from "@transportationer/shared"; import type { Poi } from "@transportationer/shared"; export const runtime = "nodejs"; export async function GET(req: NextRequest) { const p = req.nextUrl.searchParams; const city = p.get("city") ?? "berlin"; const category = p.get("category"); const bboxStr = p.get("bbox"); const cluster = p.get("cluster") === "true"; const zoom = parseInt(p.get("zoom") ?? "12", 10); if (category && !CATEGORY_IDS.includes(category as any)) { return NextResponse.json( { error: "Invalid category", code: "INVALID_CATEGORY" }, { status: 400 }, ); } const cacheKey = `api:pois:${hashParams({ city, category, bbox: bboxStr, cluster, zoom })}`; const cached = await cacheGet(cacheKey); if (cached) { return NextResponse.json(cached, { headers: { "X-Cache": "HIT" } }); } let bboxFilter = sql`TRUE`; if (bboxStr) { const parts = bboxStr.split(",").map(Number); if (parts.length !== 4 || parts.some(isNaN)) { return NextResponse.json( { error: "Invalid bbox", code: "INVALID_BBOX" }, { status: 400 }, ); } const [minLng, minLat, maxLng, maxLat] = parts; bboxFilter = sql`ST_Within(p.geom, ST_MakeEnvelope(${minLng}, ${minLat}, ${maxLng}, ${maxLat}, 4326))`; } let catFilter = sql`TRUE`; if (category) { catFilter = sql`p.category = ${category}`; } if (cluster && zoom < 14) { const rows = await Promise.resolve(sql<{ count: number; lng: number; lat: number; category: string; }[]>` SELECT COUNT(*)::int AS count, ST_X(ST_Centroid(ST_Collect(p.geom)))::float AS lng, ST_Y(ST_Centroid(ST_Collect(p.geom)))::float AS lat, p.category FROM raw_pois p WHERE p.city_slug = ${city} AND ${catFilter} AND ${bboxFilter} GROUP BY ROUND(ST_X(p.geom)::numeric, 2), ROUND(ST_Y(p.geom)::numeric, 2), p.category LIMIT 2000 `); await cacheSet(cacheKey, rows, "API_POIS"); return NextResponse.json(rows); } const rows = await Promise.resolve(sql<{ osm_id: string; osm_type: string; category: string; subcategory: string; name: string | null; lng: number; lat: number; }[]>` SELECT p.osm_id::text, p.osm_type, p.category, p.subcategory, p.name, ST_X(p.geom)::float AS lng, ST_Y(p.geom)::float AS lat FROM raw_pois p WHERE p.city_slug = ${city} AND ${catFilter} AND ${bboxFilter} LIMIT 5000 `); const pois: Poi[] = rows.map((r) => ({ osmId: r.osm_id, osmType: r.osm_type as "N" | "W" | "R", category: r.category as Poi["category"], subcategory: r.subcategory, name: r.name, lng: r.lng, lat: r.lat, })); await cacheSet(cacheKey, pois, "API_POIS"); return NextResponse.json(pois, { headers: { "Cache-Control": "public, s-maxage=300" }, }); }