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
99 lines
4.4 KiB
PL/PgSQL
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;
|