poi-index: map-core selectors, planner.pois schema, /api/pois route + client, metrics
Replace the planner's Overpass proxy with a self-hosted POI index: - map-core POI categories become structured tag selectors (single source of truth) + osmium filter / classification helpers - planner.pois PostGIS table (centroid points, GiST + category indexes) - /api/pois serving route (session + same-origin + rate limit, 100 cap) - lib/pois.ts client (renamed from overpass.ts; GET, quantized bbox) - metrics: poi_api_requests_total + DB-collected index rows/age gauges; drop overpass_* metrics - remove /api/overpass proxy + dead OVERPASS_URLS on the planner service (Journal surface backfill still uses it via code default) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7dd9ccd2b0
commit
b45e69885d
29 changed files with 887 additions and 933 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { pgSchema, text, timestamp, boolean, customType, index } from "drizzle-orm/pg-core";
|
||||
import { pgSchema, text, timestamp, boolean, jsonb, customType, index, primaryKey } from "drizzle-orm/pg-core";
|
||||
|
||||
const bytea = customType<{ data: Buffer }>({
|
||||
dataType() {
|
||||
|
|
@ -6,6 +6,15 @@ const bytea = customType<{ data: Buffer }>({
|
|||
},
|
||||
});
|
||||
|
||||
// Centroid point in WGS84. Ways/relations are reduced to their centroid at
|
||||
// extract time (matching the old Overpass `out center`), so every POI is a
|
||||
// single point regardless of its OSM geometry.
|
||||
const point = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return "geometry(Point, 4326)";
|
||||
},
|
||||
});
|
||||
|
||||
export const plannerSchema = pgSchema("planner");
|
||||
|
||||
export const sessions = plannerSchema.table("sessions", {
|
||||
|
|
@ -22,3 +31,30 @@ export const sessions = plannerSchema.table("sessions", {
|
|||
// total sessions ever created.
|
||||
lastActivityIdx: index("sessions_last_activity_idx").on(t.lastActivity),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Self-hosted POI index. Fed monthly by the extract pipeline (osmium filter on
|
||||
* the BRouter host) + the flagship import job's atomic staging swap — never
|
||||
* written at request time. Served read-only by `/api/pois` for the planner's
|
||||
* map overlays, replacing the former Overpass proxy.
|
||||
*
|
||||
* An OSM element that matches more than one category is stored once per
|
||||
* category (part of the composite primary key), because the client treats each
|
||||
* category's markers independently.
|
||||
*/
|
||||
export const pois = plannerSchema.table("pois", {
|
||||
// OSM element type: 'n' (node), 'w' (way), or 'r' (relation).
|
||||
osmType: text("osm_type").notNull(),
|
||||
osmId: text("osm_id").notNull(),
|
||||
category: text("category").notNull(),
|
||||
name: text("name"),
|
||||
geom: point("geom").notNull(),
|
||||
tags: jsonb("tags").$type<Record<string, string>>().notNull(),
|
||||
importedAt: timestamp("imported_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (t) => ({
|
||||
pk: primaryKey({ columns: [t.osmType, t.osmId, t.category] }),
|
||||
// GiST spatial index for the bbox intersection in the serving query.
|
||||
geomIdx: index("pois_geom_idx").using("gist", t.geom),
|
||||
// btree on category so `category = ANY($cats)` is index-driven.
|
||||
categoryIdx: index("pois_category_idx").on(t.category),
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue