trips/spatial/schema.sql
Greg Pomerantz c367eba5cb Live web enrichment (SearXNG) + PostGIS spatial stack
Milestone 1 — live web search:
- mock/server.js: /search + /search-status proxy to the self-hosted
  SearXNG (third leg of the 3-source blend; engine endpoint stays
  server-side), /spatial + /spatial-status proxy to the spatial service
- mock/app.js: background enrichment of suggestion cards — each
  candidate gets a web element (discovery cards + "you might like"
  rows) that is checked via SearXNG in the background (one in-flight
  query per place, 1 h TTL, failures retry in ~5 min) and APPENDS
  sourced facts with provenance URLs; never reorders/rewrites the plan.
  New LLM tools: web_search (cite URLs) and poi_near (PostGIS). Status
  badge gains 'web' / 'spatial'.
- styles.css: .cc-web live-facts blocks, t-web verified chip

Milestone 2 — PostGIS spatial DB (code complete; needs docker access
to run — see spatial/README.md):
- spatial/: docker-compose (postgis/postgis:16-3.4 + osm2pgsql:16
  one-shot importer + spatiald), schema.sql (poi table w/ GIST index,
  spatial_extract bookkeeping, refresh_poi()), import.sh for the PBF
  extracts in ../osm, spatiald (Go, lib/pq): /health, /near
  (ST_DWithin), /nearest (KNN), /corridor (ST_Buffer along a route),
  with kind/name/extract/limit filters
2026-09-10 09:43:30 -04:00

99 lines
4.4 KiB
PL/PgSQL

-- PostGIS schema for the trip planner (runs once, on first container boot).
-- osm2pgsql loads the standard planet_osm_* tables; we then materialize the
-- POI points we query on, with a GIST index for ST_DWithin / KNN.
CREATE EXTENSION IF NOT EXISTS postgis;
-- Named data sets (so multi-extract setups — e.g. northeast + colombia — can
-- be imported into one DB and disambiguated).
CREATE TABLE IF NOT EXISTS spatial_extract (
name text PRIMARY KEY,
imported_at timestamptz NOT NULL DEFAULT now(),
bounds box, -- from osmium (in degrees)
poi_count bigint
);
-- Searchable POIs: every named node/way of the interesting kinds, as a point.
-- osm2pgsql's default table schema (see README: --default-schema / stock).
CREATE TABLE IF NOT EXISTS poi (
extract text NOT NULL REFERENCES spatial_extract(name) ON DELETE CASCADE,
osm_id bigint NOT NULL,
osm_type char NOT NULL, -- 'N' | 'W'
name text NOT NULL,
kind text NOT NULL, -- dominant tag: amenity=, tourism=, …
amenity text,
tourism text,
shop text,
leisure text,
historic text,
place text,
opening_hours text,
fee text, -- OSM fee tag / fee:* values
website text,
addr_city text,
geom geometry(Point, 4326) NOT NULL,
PRIMARY KEY (extract, osm_type, osm_id)
);
CREATE INDEX IF NOT EXISTS poi_geom_gist ON poi USING GIST (geom);
CREATE INDEX IF NOT EXISTS poi_kind ON poi (extract, kind);
CREATE INDEX IF NOT EXISTS poi_name_trgm ON poi USING GIN (to_tsvector('simple', name));
-- pgvector (semantic POI index) is a later milestone: it needs the vector
-- package in the image (postgis/postgis doesn't ship it), so it gets its own
-- init script + embedding importer instead of living in this base schema.
-- Helper: refresh the poi table from a freshly loaded extract.
-- (Invoked by import.sh; kept here so the SQL lives in one place.)
CREATE OR REPLACE FUNCTION refresh_poi(p_extract text) RETURNS void AS $$
BEGIN
DELETE FROM poi WHERE extract = p_extract;
INSERT INTO poi (extract, osm_id, osm_type, name, kind, amenity, tourism, shop,
leisure, historic, place, opening_hours, fee, website, addr_city, geom)
WITH cand AS (
SELECT 'N'::char AS t, n.osm_id, n.name, n.amenity, n.tourism, n.shop,
n.leisure, n.historic, n.place, n.opening_hours, n.fee, n.website,
n.addr_city, ST_SetSRID(ST_MakePoint(n.lon, n.lat), 4326) AS geom
FROM planet_osm_node n
WHERE n.name IS NOT NULL AND n.name <> ''
AND (n.amenity IS NOT NULL OR n.tourism IS NOT NULL OR n.shop IS NOT NULL
OR n.leisure IS NOT NULL OR n.historic IS NOT NULL OR n.place IS NOT NULL)
UNION ALL
SELECT 'W'::char, w.osm_id, w.name, w.amenity, w.tourism, w.shop,
w.leisure, w.historic, w.place, w.opening_hours, w.fee, w.website,
w.addr_city, ST_Centroid(w.geom)
FROM planet_osm_way w
WHERE w.name IS NOT NULL AND w.name <> ''
AND (w.amenity IS NOT NULL OR w.tourism IS NOT NULL OR w.shop IS NOT NULL
OR w.leisure IS NOT NULL OR w.historic IS NOT NULL)
AND ST_GeometryType(w.geom) = 'ST_Polygon' -- areas only (gardens, parks…)
)
SELECT p_extract, osm_id, t, name,
COALESCE(
CASE WHEN amenity IS NOT NULL THEN 'amenity=' || amenity
END,
CASE WHEN tourism IS NOT NULL THEN 'tourism=' || tourism
END,
CASE WHEN shop IS NOT NULL THEN 'shop=' || shop
END,
CASE WHEN leisure IS NOT NULL THEN 'leisure=' || leisure
END,
CASE WHEN historic IS NOT NULL THEN 'historic=' || historic
END,
CASE WHEN place IS NOT NULL THEN 'place=' || place
END
),
amenity, tourism, shop, leisure, historic, place,
opening_hours, fee, website, addr_city, geom
FROM cand
WHERE COALESCE(amenity, tourism, shop, leisure, historic, place) IS NOT NULL;
UPDATE spatial_extract
SET poi_count = (SELECT count(*) FROM poi WHERE extract = p_extract),
imported_at = now()
WHERE name = p_extract;
ANALYZE poi;
END;
$$ LANGUAGE plpgsql;