-- PostGIS International Boundary Schema for Heritage Custodian Service Areas -- Migration: 002_postgis_boundaries.sql -- Created: 2025-12-07 -- -- Stores administrative boundaries for computing heritage custodian service areas -- Supports global coverage with multiple data sources (GADM, Natural Earth, OSM, CBS) -- Enables spatial queries: point-in-polygon, intersection, containment -- ============================================================================ -- Enable PostGIS Extension -- ============================================================================ CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS postgis_topology; -- ============================================================================ -- Data Source Registry -- ============================================================================ -- Track data sources for provenance and updates CREATE TABLE IF NOT EXISTS boundary_data_sources ( id SERIAL PRIMARY KEY, source_code VARCHAR(50) NOT NULL UNIQUE, -- e.g., "GADM", "CBS", "OSM", "HALC" source_name VARCHAR(255) NOT NULL, -- Full name source_url TEXT, -- Official website license_type VARCHAR(100), -- e.g., "CC-BY-4.0", "ODbL" license_url TEXT, description TEXT, coverage_scope VARCHAR(50), -- "GLOBAL", "EUROPE", "NETHERLANDS", etc. last_updated DATE, attribution_required TEXT, -- Required attribution text metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Insert known data sources INSERT INTO boundary_data_sources (source_code, source_name, source_url, license_type, coverage_scope, description) VALUES ('GADM', 'Global Administrative Areas Database', 'https://gadm.org', 'CC-BY-NC-4.0', 'GLOBAL', 'Global administrative boundaries at multiple levels (country, region, district)'), ('NATURAL_EARTH', 'Natural Earth', 'https://www.naturalearthdata.com', 'Public Domain', 'GLOBAL', 'Free vector and raster map data at 1:10m, 1:50m, and 1:110m scales'), ('OSM', 'OpenStreetMap', 'https://www.openstreetmap.org', 'ODbL', 'GLOBAL', 'Crowdsourced global geographic data'), ('CBS', 'CBS Wijken en Buurten', 'https://www.cbs.nl/nl-nl/dossier/nederland-regionaal/geografische-data', 'CC-BY-4.0', 'NETHERLANDS', 'Official Dutch municipality boundaries from CBS'), ('HALC', 'Historical Atlas of the Low Countries', NULL, 'Academic', 'LOW_COUNTRIES', 'Historical administrative boundaries for Netherlands, Belgium, Luxembourg'), ('EUROSTAT', 'Eurostat NUTS/LAU', 'https://ec.europa.eu/eurostat/web/gisco', 'Eurostat', 'EUROPE', 'European statistical regions (NUTS) and local administrative units (LAU)') ON CONFLICT (source_code) DO NOTHING; -- ============================================================================ -- Countries Table -- ============================================================================ -- ISO 3166-1 countries with boundaries CREATE TABLE IF NOT EXISTS boundary_countries ( id SERIAL PRIMARY KEY, iso_a2 CHAR(2) NOT NULL, -- ISO 3166-1 alpha-2 (e.g., "NL", "DE") iso_a3 CHAR(3) NOT NULL, -- ISO 3166-1 alpha-3 (e.g., "NLD", "DEU") iso_n3 CHAR(3), -- ISO 3166-1 numeric (e.g., "528") country_name VARCHAR(255) NOT NULL, -- English name country_name_local VARCHAR(255), -- Native language name -- Geometry geom GEOMETRY(MULTIPOLYGON, 4326), -- WGS84 boundary polygon centroid GEOMETRY(POINT, 4326), -- Computed centroid bbox BOX2D, -- Bounding box for quick filtering -- Metadata source_id INTEGER REFERENCES boundary_data_sources(id), source_feature_id VARCHAR(100), -- ID in source dataset area_km2 NUMERIC, -- Computed area -- Versioning valid_from DATE, valid_to DATE, -- NULL = current created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(iso_a2, valid_to) -- Allow multiple versions, only one current ); CREATE INDEX IF NOT EXISTS idx_boundary_countries_iso_a2 ON boundary_countries(iso_a2); CREATE INDEX IF NOT EXISTS idx_boundary_countries_geom ON boundary_countries USING GIST(geom); CREATE INDEX IF NOT EXISTS idx_boundary_countries_centroid ON boundary_countries USING GIST(centroid); CREATE INDEX IF NOT EXISTS idx_boundary_countries_valid ON boundary_countries(valid_from, valid_to); -- ============================================================================ -- Administrative Level 1 (States/Provinces/Regions) -- ============================================================================ -- ISO 3166-2 regions CREATE TABLE IF NOT EXISTS boundary_admin1 ( id SERIAL PRIMARY KEY, country_id INTEGER REFERENCES boundary_countries(id), -- Identifiers iso_3166_2 VARCHAR(10), -- e.g., "NL-NH", "DE-BY", "JP-13" admin1_code VARCHAR(20) NOT NULL, -- Internal code (varies by source) geonames_id INTEGER, -- GeoNames ID for linking wikidata_id VARCHAR(20), -- Wikidata Q-number -- Names admin1_name VARCHAR(255) NOT NULL, -- English/standard name admin1_name_local VARCHAR(255), -- Native language name admin1_type VARCHAR(100), -- e.g., "Province", "State", "Prefecture", "Land" -- Geometry geom GEOMETRY(MULTIPOLYGON, 4326), centroid GEOMETRY(POINT, 4326), bbox BOX2D, -- Metadata source_id INTEGER REFERENCES boundary_data_sources(id), source_feature_id VARCHAR(100), area_km2 NUMERIC, -- Versioning valid_from DATE, valid_to DATE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(country_id, admin1_code, valid_to) ); CREATE INDEX IF NOT EXISTS idx_boundary_admin1_country ON boundary_admin1(country_id); CREATE INDEX IF NOT EXISTS idx_boundary_admin1_iso ON boundary_admin1(iso_3166_2); CREATE INDEX IF NOT EXISTS idx_boundary_admin1_geonames ON boundary_admin1(geonames_id); CREATE INDEX IF NOT EXISTS idx_boundary_admin1_geom ON boundary_admin1 USING GIST(geom); CREATE INDEX IF NOT EXISTS idx_boundary_admin1_centroid ON boundary_admin1 USING GIST(centroid); -- ============================================================================ -- Administrative Level 2 (Districts/Counties/Municipalities) -- ============================================================================ CREATE TABLE IF NOT EXISTS boundary_admin2 ( id SERIAL PRIMARY KEY, admin1_id INTEGER REFERENCES boundary_admin1(id), country_id INTEGER REFERENCES boundary_countries(id), -- Identifiers admin2_code VARCHAR(50) NOT NULL, -- Internal code nuts_code VARCHAR(10), -- NUTS code (EU) lau_code VARCHAR(20), -- LAU code (EU) geonames_id INTEGER, wikidata_id VARCHAR(20), -- CBS-specific (Netherlands) cbs_gemeente_code VARCHAR(10), -- Dutch municipality code (GM0363) -- Names admin2_name VARCHAR(255) NOT NULL, admin2_name_local VARCHAR(255), admin2_type VARCHAR(100), -- e.g., "Municipality", "District", "County" -- Geometry geom GEOMETRY(MULTIPOLYGON, 4326), centroid GEOMETRY(POINT, 4326), bbox BOX2D, -- Metadata source_id INTEGER REFERENCES boundary_data_sources(id), source_feature_id VARCHAR(100), area_km2 NUMERIC, population INTEGER, -- If available from source -- Versioning valid_from DATE, valid_to DATE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(admin1_id, admin2_code, valid_to) ); CREATE INDEX IF NOT EXISTS idx_boundary_admin2_admin1 ON boundary_admin2(admin1_id); CREATE INDEX IF NOT EXISTS idx_boundary_admin2_country ON boundary_admin2(country_id); CREATE INDEX IF NOT EXISTS idx_boundary_admin2_geonames ON boundary_admin2(geonames_id); CREATE INDEX IF NOT EXISTS idx_boundary_admin2_cbs ON boundary_admin2(cbs_gemeente_code); CREATE INDEX IF NOT EXISTS idx_boundary_admin2_geom ON boundary_admin2 USING GIST(geom); CREATE INDEX IF NOT EXISTS idx_boundary_admin2_centroid ON boundary_admin2 USING GIST(centroid); -- ============================================================================ -- Historical Boundaries -- ============================================================================ -- Pre-modern administrative boundaries (e.g., HALC 1500 data) CREATE TABLE IF NOT EXISTS boundary_historical ( id SERIAL PRIMARY KEY, -- Identifiers historical_id VARCHAR(100) NOT NULL, -- Source-specific ID halc_adm1_code VARCHAR(20), -- HALC ADM1 territory code (e.g., "VI") halc_adm2_name VARCHAR(255), -- HALC ADM2 district name wikidata_id VARCHAR(20), -- Location context modern_country_iso_a2 CHAR(2), -- Modern country this is within -- Names territory_name VARCHAR(255) NOT NULL, territory_name_historical VARCHAR(255), -- Name in historical period territory_type VARCHAR(100), -- e.g., "County", "Duchy", "Lordship" -- Temporal extent period_start SMALLINT, -- Year (e.g., 1500) period_end SMALLINT, -- Year (e.g., 1795) period_description VARCHAR(255), -- e.g., "Ancien Regime", "Medieval" -- Geometry geom GEOMETRY(MULTIPOLYGON, 4326), centroid GEOMETRY(POINT, 4326), bbox BOX2D, -- Metadata source_id INTEGER REFERENCES boundary_data_sources(id), source_feature_id VARCHAR(100), area_km2 NUMERIC, certainty VARCHAR(50), -- "HIGH", "MEDIUM", "LOW", "APPROXIMATE" notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(historical_id, source_id) ); CREATE INDEX IF NOT EXISTS idx_boundary_historical_halc ON boundary_historical(halc_adm1_code); CREATE INDEX IF NOT EXISTS idx_boundary_historical_country ON boundary_historical(modern_country_iso_a2); CREATE INDEX IF NOT EXISTS idx_boundary_historical_period ON boundary_historical(period_start, period_end); CREATE INDEX IF NOT EXISTS idx_boundary_historical_geom ON boundary_historical USING GIST(geom); -- ============================================================================ -- Custodian Service Areas (Computed/Custom) -- ============================================================================ -- Stores pre-computed or custom service area geometries for heritage custodians CREATE TABLE IF NOT EXISTS custodian_service_areas ( id SERIAL PRIMARY KEY, -- Custodian link (GHCID) ghcid VARCHAR(100) NOT NULL, -- e.g., "NL-NH-HAA-A-NHA" ghcid_uuid UUID, -- UUID v5 of GHCID -- Service area identity service_area_name VARCHAR(255) NOT NULL, service_area_type VARCHAR(50) NOT NULL, -- From ServiceAreaTypeEnum -- Geometry (can be computed union or custom-drawn) geom GEOMETRY(MULTIPOLYGON, 4326), centroid GEOMETRY(POINT, 4326), bbox BOX2D, -- Composition (which admin units make up this service area) admin2_ids INTEGER[], -- Array of boundary_admin2.id admin1_ids INTEGER[], -- Array of boundary_admin1.id historical_ids INTEGER[], -- Array of boundary_historical.id -- Properties is_historical BOOLEAN DEFAULT FALSE, is_custom BOOLEAN DEFAULT FALSE, -- True if hand-drawn, not computed from admin units -- Temporal extent valid_from DATE, valid_to DATE, -- NULL = current -- Display properties display_fill_color VARCHAR(20) DEFAULT '#3498db', display_fill_opacity NUMERIC(3,2) DEFAULT 0.20, display_line_color VARCHAR(20) DEFAULT '#2980b9', display_line_width NUMERIC(3,1) DEFAULT 2.0, -- Metadata source_dataset VARCHAR(100), -- Data provenance notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(ghcid, valid_to) ); CREATE INDEX IF NOT EXISTS idx_custodian_sa_ghcid ON custodian_service_areas(ghcid); CREATE INDEX IF NOT EXISTS idx_custodian_sa_uuid ON custodian_service_areas(ghcid_uuid); CREATE INDEX IF NOT EXISTS idx_custodian_sa_geom ON custodian_service_areas USING GIST(geom); CREATE INDEX IF NOT EXISTS idx_custodian_sa_type ON custodian_service_areas(service_area_type); CREATE INDEX IF NOT EXISTS idx_custodian_sa_historical ON custodian_service_areas(is_historical); -- ============================================================================ -- GeoNames Settlements (for reverse geocoding and linking) -- ============================================================================ -- Subset of GeoNames populated places for settlement lookups CREATE TABLE IF NOT EXISTS geonames_settlements ( geonames_id INTEGER PRIMARY KEY, -- Names name VARCHAR(200) NOT NULL, ascii_name VARCHAR(200), alternate_names TEXT, -- Comma-separated -- Location latitude NUMERIC(10, 6) NOT NULL, longitude NUMERIC(10, 6) NOT NULL, geom GEOMETRY(POINT, 4326), -- Computed from lat/lon -- Classification feature_class CHAR(1), -- 'P' = populated place feature_code VARCHAR(10), -- 'PPL', 'PPLA', 'PPLC', etc. -- Administrative hierarchy country_code CHAR(2), admin1_code VARCHAR(20), -- GeoNames admin1 admin2_code VARCHAR(80), admin3_code VARCHAR(20), admin4_code VARCHAR(20), -- Metadata population INTEGER, elevation INTEGER, timezone VARCHAR(40), modification_date DATE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_geonames_country ON geonames_settlements(country_code); CREATE INDEX IF NOT EXISTS idx_geonames_admin1 ON geonames_settlements(country_code, admin1_code); CREATE INDEX IF NOT EXISTS idx_geonames_feature ON geonames_settlements(feature_class, feature_code); CREATE INDEX IF NOT EXISTS idx_geonames_geom ON geonames_settlements USING GIST(geom); CREATE INDEX IF NOT EXISTS idx_geonames_name ON geonames_settlements(name); CREATE INDEX IF NOT EXISTS idx_geonames_ascii ON geonames_settlements(ascii_name); CREATE INDEX IF NOT EXISTS idx_geonames_population ON geonames_settlements(population DESC); -- ============================================================================ -- Spatial Functions -- ============================================================================ -- Function: Find admin units containing a point CREATE OR REPLACE FUNCTION find_admin_for_point( p_lon NUMERIC, p_lat NUMERIC, p_country_code CHAR(2) DEFAULT NULL ) RETURNS TABLE ( admin_level INTEGER, admin_code VARCHAR, admin_name VARCHAR, iso_code VARCHAR, geonames_id INTEGER ) AS $$ BEGIN RETURN QUERY -- Admin Level 1 SELECT 1::INTEGER AS admin_level, a.admin1_code, a.admin1_name, a.iso_3166_2, a.geonames_id FROM boundary_admin1 a JOIN boundary_countries c ON a.country_id = c.id WHERE ST_Contains(a.geom, ST_SetSRID(ST_Point(p_lon, p_lat), 4326)) AND a.valid_to IS NULL AND (p_country_code IS NULL OR c.iso_a2 = p_country_code) UNION ALL -- Admin Level 2 SELECT 2::INTEGER, a.admin2_code, a.admin2_name, a.nuts_code, a.geonames_id FROM boundary_admin2 a JOIN boundary_countries c ON a.country_id = c.id WHERE ST_Contains(a.geom, ST_SetSRID(ST_Point(p_lon, p_lat), 4326)) AND a.valid_to IS NULL AND (p_country_code IS NULL OR c.iso_a2 = p_country_code) ORDER BY admin_level; END; $$ LANGUAGE plpgsql; -- Function: Find nearest settlement CREATE OR REPLACE FUNCTION find_nearest_settlement( p_lon NUMERIC, p_lat NUMERIC, p_country_code CHAR(2) DEFAULT NULL, p_max_distance_km NUMERIC DEFAULT 50 ) RETURNS TABLE ( geonames_id INTEGER, name VARCHAR, feature_code VARCHAR, population INTEGER, distance_km NUMERIC ) AS $$ DECLARE v_point GEOMETRY; BEGIN v_point := ST_SetSRID(ST_Point(p_lon, p_lat), 4326); RETURN QUERY SELECT g.geonames_id, g.name, g.feature_code, g.population, ROUND((ST_Distance(g.geom::geography, v_point::geography) / 1000)::NUMERIC, 2) AS distance_km FROM geonames_settlements g WHERE g.feature_class = 'P' AND g.feature_code IN ('PPL', 'PPLA', 'PPLA2', 'PPLA3', 'PPLA4', 'PPLC', 'PPLS', 'PPLG') AND (p_country_code IS NULL OR g.country_code = p_country_code) AND ST_DWithin(g.geom::geography, v_point::geography, p_max_distance_km * 1000) ORDER BY g.geom <-> v_point LIMIT 5; END; $$ LANGUAGE plpgsql; -- Function: Compute service area from admin units CREATE OR REPLACE FUNCTION compute_service_area_geometry( p_admin2_ids INTEGER[], p_admin1_ids INTEGER[] DEFAULT NULL ) RETURNS GEOMETRY AS $$ DECLARE v_geom GEOMETRY; BEGIN -- Union all admin2 geometries IF p_admin2_ids IS NOT NULL AND array_length(p_admin2_ids, 1) > 0 THEN SELECT ST_Union(geom) INTO v_geom FROM boundary_admin2 WHERE id = ANY(p_admin2_ids) AND valid_to IS NULL; END IF; -- Add admin1 geometries if specified IF p_admin1_ids IS NOT NULL AND array_length(p_admin1_ids, 1) > 0 THEN SELECT ST_Union(COALESCE(v_geom, 'GEOMETRYCOLLECTION EMPTY'::GEOMETRY), ST_Union(geom)) INTO v_geom FROM boundary_admin1 WHERE id = ANY(p_admin1_ids) AND valid_to IS NULL; END IF; RETURN v_geom; END; $$ LANGUAGE plpgsql; -- Function: Get service area GeoJSON for a custodian CREATE OR REPLACE FUNCTION get_custodian_service_area_geojson( p_ghcid VARCHAR, p_include_historical BOOLEAN DEFAULT FALSE ) RETURNS JSONB AS $$ DECLARE v_result JSONB; BEGIN SELECT jsonb_build_object( 'type', 'FeatureCollection', 'features', COALESCE(jsonb_agg( jsonb_build_object( 'type', 'Feature', 'geometry', ST_AsGeoJSON(sa.geom)::jsonb, 'properties', jsonb_build_object( 'ghcid', sa.ghcid, 'name', sa.service_area_name, 'type', sa.service_area_type, 'is_historical', sa.is_historical, 'valid_from', sa.valid_from, 'valid_to', sa.valid_to, 'fill_color', sa.display_fill_color, 'fill_opacity', sa.display_fill_opacity, 'line_color', sa.display_line_color, 'line_width', sa.display_line_width ) ) ), '[]'::jsonb) ) INTO v_result FROM custodian_service_areas sa WHERE sa.ghcid = p_ghcid AND (p_include_historical OR sa.is_historical = FALSE); RETURN v_result; END; $$ LANGUAGE plpgsql; -- ============================================================================ -- Views for API Access -- ============================================================================ -- Current admin1 boundaries CREATE OR REPLACE VIEW v_current_admin1 AS SELECT a.id, c.iso_a2 AS country_code, a.iso_3166_2, a.admin1_code, a.admin1_name, a.admin1_type, a.geonames_id, a.wikidata_id, ST_AsGeoJSON(a.geom)::jsonb AS geometry, ST_AsGeoJSON(a.centroid)::jsonb AS centroid, a.area_km2, s.source_code AS data_source FROM boundary_admin1 a JOIN boundary_countries c ON a.country_id = c.id LEFT JOIN boundary_data_sources s ON a.source_id = s.id WHERE a.valid_to IS NULL AND c.valid_to IS NULL; -- Current admin2 boundaries CREATE OR REPLACE VIEW v_current_admin2 AS SELECT a2.id, c.iso_a2 AS country_code, a1.iso_3166_2 AS admin1_iso, a2.admin2_code, a2.admin2_name, a2.admin2_type, a2.cbs_gemeente_code, a2.nuts_code, a2.lau_code, a2.geonames_id, a2.wikidata_id, ST_AsGeoJSON(a2.geom)::jsonb AS geometry, ST_AsGeoJSON(a2.centroid)::jsonb AS centroid, a2.area_km2, a2.population, s.source_code AS data_source FROM boundary_admin2 a2 JOIN boundary_admin1 a1 ON a2.admin1_id = a1.id JOIN boundary_countries c ON a2.country_id = c.id LEFT JOIN boundary_data_sources s ON a2.source_id = s.id WHERE a2.valid_to IS NULL AND a1.valid_to IS NULL AND c.valid_to IS NULL; -- Custodian service areas CREATE OR REPLACE VIEW v_custodian_service_areas AS SELECT sa.ghcid, sa.ghcid_uuid, sa.service_area_name, sa.service_area_type, sa.is_historical, sa.is_custom, sa.valid_from, sa.valid_to, ST_AsGeoJSON(sa.geom)::jsonb AS geometry, ST_AsGeoJSON(sa.centroid)::jsonb AS centroid, sa.display_fill_color, sa.display_fill_opacity, sa.display_line_color, sa.display_line_width, sa.source_dataset, sa.notes FROM custodian_service_areas sa WHERE sa.valid_to IS NULL; -- ============================================================================ -- Statistics -- ============================================================================ CREATE OR REPLACE VIEW v_boundary_stats AS SELECT 'countries' AS table_name, COUNT(*) AS total_rows, COUNT(*) FILTER (WHERE valid_to IS NULL) AS current_rows FROM boundary_countries UNION ALL SELECT 'admin1', COUNT(*), COUNT(*) FILTER (WHERE valid_to IS NULL) FROM boundary_admin1 UNION ALL SELECT 'admin2', COUNT(*), COUNT(*) FILTER (WHERE valid_to IS NULL) FROM boundary_admin2 UNION ALL SELECT 'historical', COUNT(*), COUNT(*) FROM boundary_historical UNION ALL SELECT 'service_areas', COUNT(*), COUNT(*) FILTER (WHERE valid_to IS NULL) FROM custodian_service_areas UNION ALL SELECT 'geonames_settlements', COUNT(*), COUNT(*) FROM geonames_settlements; -- Country coverage summary CREATE OR REPLACE VIEW v_boundary_coverage AS SELECT c.iso_a2, c.country_name, COUNT(DISTINCT a1.id) AS admin1_count, COUNT(DISTINCT a2.id) AS admin2_count, (SELECT COUNT(*) FROM geonames_settlements g WHERE g.country_code = c.iso_a2) AS settlement_count, (SELECT COUNT(*) FROM custodian_service_areas sa WHERE sa.ghcid LIKE c.iso_a2 || '-%') AS service_area_count FROM boundary_countries c LEFT JOIN boundary_admin1 a1 ON a1.country_id = c.id AND a1.valid_to IS NULL LEFT JOIN boundary_admin2 a2 ON a2.country_id = c.id AND a2.valid_to IS NULL WHERE c.valid_to IS NULL GROUP BY c.id, c.iso_a2, c.country_name ORDER BY c.country_name; -- ============================================================================ -- Permissions -- ============================================================================ GRANT SELECT ON ALL TABLES IN SCHEMA public TO glam_api; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO glam_api; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO glam_api; -- ============================================================================ -- Table Comments -- ============================================================================ COMMENT ON TABLE boundary_data_sources IS 'Registry of data sources for boundary data (GADM, CBS, OSM, etc.)'; COMMENT ON TABLE boundary_countries IS 'Country boundaries with ISO 3166-1 codes'; COMMENT ON TABLE boundary_admin1 IS 'First-level administrative divisions (states, provinces, regions)'; COMMENT ON TABLE boundary_admin2 IS 'Second-level administrative divisions (municipalities, districts, counties)'; COMMENT ON TABLE boundary_historical IS 'Historical administrative boundaries (e.g., HALC 1500 data)'; COMMENT ON TABLE custodian_service_areas IS 'Pre-computed or custom service area geometries for heritage custodians'; COMMENT ON TABLE geonames_settlements IS 'GeoNames populated places for settlement lookups'; COMMENT ON FUNCTION find_admin_for_point IS 'Find administrative units containing a geographic point'; COMMENT ON FUNCTION find_nearest_settlement IS 'Find nearest GeoNames settlement to a point'; COMMENT ON FUNCTION compute_service_area_geometry IS 'Compute union geometry from admin unit IDs'; COMMENT ON FUNCTION get_custodian_service_area_geojson IS 'Get GeoJSON for a custodian service area';