-- 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;