-- BikepackPilot database draft -- Requires PostgreSQL + PostGIS. CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TYPE confidence_level AS ENUM ( 'known_good', 'high', 'medium', 'low', 'unknown', 'restricted', 'conflicting' ); CREATE TYPE warning_severity AS ENUM ('info', 'notice', 'warning', 'critical'); CREATE TYPE poi_category AS ENUM ( 'water', 'food', 'sleep', 'repair', 'bailout', 'charging', 'emergency' ); CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT UNIQUE, display_name TEXT, default_profile TEXT NOT NULL DEFAULT 'loaded_gravel', privacy_mode TEXT NOT NULL DEFAULT 'private', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE trips ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID REFERENCES users(id) ON DELETE SET NULL, title TEXT NOT NULL, start_point GEOGRAPHY(Point, 4326) NOT NULL, end_point GEOGRAPHY(Point, 4326) NOT NULL, start_date DATE, end_date DATE, profile TEXT NOT NULL, constraints JSONB NOT NULL DEFAULT '{}'::jsonb, status TEXT NOT NULL DEFAULT 'draft', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE routes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), trip_id UUID NOT NULL REFERENCES trips(id) ON DELETE CASCADE, provider TEXT NOT NULL, geometry GEOGRAPHY(LineString, 4326) NOT NULL, distance_m INTEGER NOT NULL, ascent_m INTEGER DEFAULT 0, descent_m INTEGER DEFAULT 0, surface_breakdown JSONB NOT NULL DEFAULT '{}'::jsonb, suitability_score NUMERIC(5,2), warnings JSONB NOT NULL DEFAULT '[]'::jsonb, attribution TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE route_segments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), route_id UUID NOT NULL REFERENCES routes(id) ON DELETE CASCADE, seq INTEGER NOT NULL, start_m INTEGER NOT NULL, end_m INTEGER NOT NULL, geometry GEOGRAPHY(LineString, 4326) NOT NULL, distance_m INTEGER NOT NULL, ascent_m INTEGER DEFAULT 0, descent_m INTEGER DEFAULT 0, avg_grade_percent NUMERIC(5,2), max_grade_percent NUMERIC(5,2), surface TEXT, smoothness TEXT, highway TEXT, bicycle_access TEXT, legal_access_confidence confidence_level NOT NULL DEFAULT 'unknown', source_confidence confidence_level NOT NULL DEFAULT 'unknown', traffic_stress NUMERIC(4,2), official_cycle_route BOOLEAN DEFAULT false, protected_area_overlap BOOLEAN DEFAULT false, scores JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX route_segments_route_seq_idx ON route_segments(route_id, seq); CREATE INDEX route_segments_geom_idx ON route_segments USING GIST(geometry); CREATE TABLE pois ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category poi_category NOT NULL, name TEXT, location GEOGRAPHY(Point, 4326) NOT NULL, source TEXT NOT NULL, source_id TEXT, confidence confidence_level NOT NULL DEFAULT 'unknown', opening_hours TEXT, last_seen_at TIMESTAMPTZ, last_confirmed_by_user_at TIMESTAMPTZ, tags JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX pois_category_idx ON pois(category); CREATE INDEX pois_location_idx ON pois USING GIST(location); CREATE UNIQUE INDEX pois_source_source_id_idx ON pois(source, source_id) WHERE source_id IS NOT NULL; CREATE TABLE route_pois ( route_id UUID NOT NULL REFERENCES routes(id) ON DELETE CASCADE, poi_id UUID NOT NULL REFERENCES pois(id) ON DELETE CASCADE, meters_from_start INTEGER, distance_from_route_m INTEGER, detour_distance_m INTEGER, relevance_score NUMERIC(5,2), PRIMARY KEY (route_id, poi_id) ); CREATE TABLE stages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), route_id UUID NOT NULL REFERENCES routes(id) ON DELETE CASCADE, day_index INTEGER NOT NULL, start_m INTEGER NOT NULL, end_m INTEGER NOT NULL, distance_m INTEGER NOT NULL, ascent_m INTEGER DEFAULT 0, descent_m INTEGER DEFAULT 0, endpoint GEOGRAPHY(Point, 4326) NOT NULL, sleep_candidates JSONB NOT NULL DEFAULT '[]'::jsonb, water_pois JSONB NOT NULL DEFAULT '[]'::jsonb, food_pois JSONB NOT NULL DEFAULT '[]'::jsonb, repair_pois JSONB NOT NULL DEFAULT '[]'::jsonb, bailout_pois JSONB NOT NULL DEFAULT '[]'::jsonb, warnings JSONB NOT NULL DEFAULT '[]'::jsonb, plan_b JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE service_gaps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), route_id UUID NOT NULL REFERENCES routes(id) ON DELETE CASCADE, category poi_category NOT NULL, start_m INTEGER NOT NULL, end_m INTEGER NOT NULL, distance_m INTEGER NOT NULL, severity warning_severity NOT NULL, explanation TEXT NOT NULL ); CREATE TABLE rule_cards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), rule_type TEXT NOT NULL, jurisdiction TEXT NOT NULL, status TEXT NOT NULL, confidence confidence_level NOT NULL DEFAULT 'unknown', summary TEXT NOT NULL, source_url TEXT, source_name TEXT, last_reviewed_at DATE, valid_from DATE, valid_to DATE, geometry GEOGRAPHY(Geometry, 4326), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX rule_cards_geom_idx ON rule_cards USING GIST(geometry); CREATE TABLE local_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE SET NULL, route_id UUID REFERENCES routes(id) ON DELETE SET NULL, route_segment_id UUID REFERENCES route_segments(id) ON DELETE SET NULL, report_type TEXT NOT NULL, location GEOGRAPHY(Point, 4326) NOT NULL, meters_from_start INTEGER, observed_at TIMESTAMPTZ NOT NULL, details TEXT, payload JSONB NOT NULL DEFAULT '{}'::jsonb, trust_score NUMERIC(5,2) DEFAULT 0, expires_at TIMESTAMPTZ, moderation_status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX local_reports_location_idx ON local_reports USING GIST(location); CREATE INDEX local_reports_type_idx ON local_reports(report_type); CREATE TABLE offline_packs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), trip_id UUID NOT NULL REFERENCES trips(id) ON DELETE CASCADE, route_id UUID NOT NULL REFERENCES routes(id) ON DELETE CASCADE, bbox GEOGRAPHY(Polygon, 4326), route_buffer_km NUMERIC(6,2) NOT NULL DEFAULT 5, included_layers JSONB NOT NULL DEFAULT '[]'::jsonb, data_versions JSONB NOT NULL DEFAULT '{}'::jsonb, files JSONB NOT NULL DEFAULT '[]'::jsonb, warnings JSONB NOT NULL DEFAULT '[]'::jsonb, status TEXT NOT NULL DEFAULT 'queued', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ );