-- 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; -- osm2pgsql --hstore puts every tag that has no dedicated column here CREATE EXTENSION IF NOT EXISTS hstore; -- 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 point/polygon of the interesting kinds. -- osm2pgsql 2.x pgsql output schema: planet_osm_point / planet_osm_polygon, -- geometry in EPSG:3857 in a column called `way`; tags without a dedicated -- column (opening_hours, fee, website, addr:*) live in the hstore `tags`. 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, -- reprojected from 3857 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); -- name matching for the /search endpoint, two complementary indexes: -- * word-level full-text (to_tsvector) — exact word hits CREATE INDEX IF NOT EXISTS poi_name_fts ON poi USING GIN (to_tsvector('simple', name)); -- * trigram (fuzzy / substring, language-agnostic) — needs pg_trgm CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX IF NOT EXISTS poi_name_trgm_ops ON poi USING gin (name gin_trgm_ops); -- pgvector (opt-in semantic index) is separate: 20-vector.sql + embed/. -- 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.tags->'opening_hours' AS opening_hours, COALESCE(n.tags->'fee', n.tags->'fee:tourism') AS fee, COALESCE(n.tags->'website', n.tags->'url') AS website, n.tags->'addr:city' AS addr_city, ST_Transform(n.way, 4326) AS geom FROM planet_osm_point 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 -- relation-derived polygons repeat per relation (osm_id negated) — -- keep one row per way id SELECT * FROM ( SELECT DISTINCT ON (w.osm_id) 'W'::char AS t, w.osm_id, w.name, w.amenity, w.tourism, w.shop, w.leisure, w.historic, w.place, w.tags->'opening_hours' AS opening_hours, COALESCE(w.tags->'fee', w.tags->'fee:tourism') AS fee, COALESCE(w.tags->'website', w.tags->'url') AS website, w.tags->'addr:city' AS addr_city, ST_Centroid(ST_Transform(w.way, 4326)) AS geom FROM planet_osm_polygon 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) ORDER BY w.osm_id ) w ) 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;