Phase 3 introduces the first production local project store for Meridian.
.gispad PackageProjects are document package directories:
ProjectName.gispad/
project.json
basemaps/
data/
project.sqlite
previews/
cache/
render/
project.json is the quick-open manifest and UI state cache. data/project.sqlite is authoritative for layers, features, exact geometry, properties, summaries, warnings, and spatial indexes. basemaps/ stores imported local PMTiles files copied into the package. cache/ is rebuildable and is omitted from duplicate/share copies.
The manifest uses schema: "com.gispad.project" and schemaVersion: 1. It stores project identity, display name, timestamps, storage schema version, viewport, basemap, and layer order/visibility/style/label cache. Newer schema versions are rejected as recoverable unsupported-project errors.
SQLite uses WAL, foreign keys, a 5 second busy timeout, and RTree:
project_meta stores project identity and schema metadata.layers stores layer metadata, bounds, summaries, source file metadata, CRS warnings, dirty state, style JSON, and label JSON.features stores exact GeoJSON geometry and properties with stable internal UUID feature IDs.feature_rtree indexes feature bounds for viewport queries.fields and feature_property_values support field summaries and future table/search workflows.layer_warnings stores import warnings such as unsupported CRS metadata.SQLite v2 adds internal edit-session tables:
edit_sessions stores one recoverable active session scoped to one editable layer.edit_draft_features stores uncommitted new, moved, updated, or deleted feature drafts with exact geometry and draft bounds.edit_operation_log stores undo/redo snapshots for the active session.Authoritative features rows remain unchanged until commitEdits applies all draft rows in one transaction. revertEdits deletes the session journal and leaves stored features unchanged. Draft tables are private implementation details and are never exported as GeoJSON features.
SQLite v3 adds validation persistence:
validation_runs records layer/project validation counts and checked feature counts.validation_issues stores layer/feature issue severity, code, message, optional geometry path/coordinate/segment JSON, blocking status, and timestamp.Layer summaries expose crsStatus, validationErrorCount, and validationWarningCount so Swift can show warnings without scanning full layers on the main thread.
Phase 6.5 does not introduce SQLite v4. WKP is benchmarked as a derived geometry codec only; no WKP blob/string is stored in production features rows. If a future phase adopts WKP, it must be a rebuildable cache keyed by authoritative geometry_json hash, precision, and dimensions, not the source of truth.
Layer style and label rules are stored in SQLite and mirrored into project.json during manifest sync so quick-open UI can reconstruct layer rows without reparsing the store. Phase 4 visual metadata changes, including visibility, lock, order, opacity, swatch/style, and labels, autosave through project services. Data/edit dirty state remains separate from these visual settings.
Basemap configuration is manifest-backed. It records one of:
noneGrid: no-basemap mode with the generated two-scale longitude/latitude grid.onlineStyleURL: a MapLibre style URL plus optional attribution. Invalid URL strings are rejected before persistence, and failed style loads persist a noneGrid fallback with failure status so editable data remains usable on reopen.localPMTiles: a copied local PMTiles file under basemaps/ plus optional attribution and failure status.Swift starts import through LiveProjectService, which calls the Rust C ABI on a detached task. Rust streams GeoJSON features instead of loading the whole document into memory. The first pass computes source hash, bounds, geometry types, warnings, and field summaries; the second pass writes exact geometry and properties inside a single SQLite transaction. Internal feature IDs are assigned during the write pass, RTree rows are maintained through triggers, and failure or cancellation rolls back partial writes.
Rust reports import progress back through the production C ABI with phase, bytes read, total bytes, and feature count. Swift exposes those callbacks as ProjectImportEvent.progress. Cancellation is checked during streaming parse/write work and maps to a recoverable importCancelled service error.
Supported editable geometries are Point, MultiPoint, LineString, MultiLineString, Polygon, and MultiPolygon. V1 assumes WGS84/EPSG:4326. Legacy WGS84/CRS84 crs metadata is surfaced as legacyWGS84; unsupported CRS metadata is surfaced as unsupportedCRS. Coordinates are not transformed.
Viewport queries use SQLite RTree as a coarse bounding-box accelerator and return exact stored geometry JSON. Selected and actively edited features use exact feature lookup/overlays rather than render-simplified data.
Phase 4 adds project-store APIs for layer metadata updates, atomic layer reorder, basemap get/set, hit testing, attribute page queries with search/sort, and attribute row lookup by feature ID. Phase 5 upgrades hit testing from bounding-box candidate ranking to exact geometry distance checks. These APIs are exposed through the production Rust C ABI and Swift ProjectDataServicing; UI code should not parse original source GeoJSON.
Phase 5 adds EditingServicing over the same Rust C ABI for beginning/resuming edit sessions, applying edit operations, undo/redo, commit/revert, snapping, and draft validation. Phase 6 makes draft validation recoverable: structural parse failures reject operations, while geometry validity errors can persist in drafts and block Save Edits until fixed. Snapping queries exact stored geometry_json plus active edit-session drafts, not simplified render payloads. The Swift workspace blocks export/share while local drawing, pending New Feature, or uncommitted edit-session work is active.
Phase 6 adds Rust validation and measurement APIs over the project ABI:
validateLayer and validateProject refresh validation issue caches off the main thread.measureGeometry and measureFeature return WGS84 geodesic length/perimeter/area and vertex counts.exportPreflight validates the target layer before clean data export.Validation covers empty/malformed positions, unsupported geometry, line/ring minimums, ring closure, polygon hole containment/intersection, self-intersection, duplicate vertices, antimeridian-sensitive segments, and right-hand-rule winding warnings. Antimeridian handling is warning-aware only; V1 does not automatically cut geometry.
Phase 6.5 adds an experimental Rust WKP codec and benchmark CLI. The codec can round-trip supported V1 geometry shapes, but raw WKP carries no Feature ID, properties, CRS metadata, bbox, validation state, source layer information, or package metadata. Authoritative import/query/edit/export APIs continue to use exact GeoJSON geometry strings; scoped CSV WKP fields are derived interchange output.
Attribute table queries page through SQLite instead of building all rows in SwiftUI. The current SwiftUI table renders a fixed row-height virtual window over the active page so large layers do not create large view trees.
SQLite schema v11 adds reproducible local analysis history so users can inspect past runs, detect stale results, and rerun any previous analysis without network access.
analysis_runs stores one row per completed or failed run: run_id, tool_id, status, output_layer_id (SET NULL on delete), output_table_id (SET NULL on delete), request_json (full analysis recipe), result_json (snapshot of output IDs and counts at completion time), input_count, output_count, warning_count, error_count, started_at, completed_at, and elapsed_ms.
analysis_run_inputs stores one row per input per run: run_id (FK with cascade), role, layer_id (nullable TEXT — no FK cascade), selection_scope, selected_count, and input_layer_name (name captured at run time for display when the layer is later deleted or renamed).
analysis_issues stores per-run warnings, errors, and info messages.
selection_sets and selection_set_features support named selection scopes that can be referenced by future analysis tools.
Input staleness is detected by LEFT JOIN: if a row in analysis_run_inputs has a non-null layer_id but no matching row in layers, layer_exists is false and the status becomes inputMissing.
Output staleness cannot rely solely on the live output_layer_id / output_table_id columns in analysis_runs because both columns use ON DELETE SET NULL — they are cleared when the referenced output is deleted. Instead, the detail API reads result_json to recover the IDs that were present at completion time and checks whether those IDs still exist in the store. If they do not, the status becomes outputMissing. The result_json column is never modified after a run completes, making it a reliable snapshot.
analysis_run_inputs.layer_id originally carried an ON DELETE CASCADE foreign key to layers, which silently deleted input history rows when a layer was removed. V11 removes this constraint by recreating the table using the SQLite create-copy-drop-rename pattern under PRAGMA foreign_keys = OFF. Existing rows are copied with their input_layer_name backfilled from the live layers table where the layer still exists.
request_json stores only safe, non-credential analysis parameters (tool ID, preset, bbox, buffer distance, field names, thresholds). extract_safe_parameters in Rust explicitly excludes API keys, authorization headers, credential-bearing URLs, local absolute paths, raw debug errors, and raw source metadata. result_json stores output IDs, feature counts, and public attribution only. Analysis history is entirely local: it is stored inside the .gispad project package and does not require network access.
analysis_rerun reads the stored request_json from any completed run, assigns a new job_id and run_id, and calls the same analysis_run_with_observer path used for a fresh run. The original run is preserved. Cancellation and failure roll back the new run without affecting the original.
New Swift types: AnalysisRunDetail, AnalysisStalenessStatus (.current, .inputMissing, .outputMissing), AnalysisInputSnapshot, AnalysisOutputReference, AnalysisRunParameter.
New AnalysisServicing protocol methods: analysisRunDetail(runID:in:) and rerunAnalysis(runID:in:).
AnalysisHistorySheet provides a master-detail SwiftUI sheet: the list shows all runs with status, staleness badge, elapsed time, and tool name; the detail view shows inputs, outputs, parameters, and issues with a rerun action. The sheet is available on both iPad and Mac via shared SwiftUI + Rust (see platform parity).
Every imported feature gets an internal UUID feature_id, while SQLite keeps a private integer feature_pk for joins and RTree. Clean data export never writes either internal ID, draft/session IDs, render helper state, validation rows, or legacy crs metadata. GeoJSON restores a source top-level GeoJSON id unchanged; KML and CSV + WKT/WKP expose that public source ID as geojson_id when present. User properties, including a source fid property, are preserved as data unless the user chooses geometry-only CSV + WKT/WKP export.
Layer export runs validation preflight first. Blocking errors fail with exportFailed; warnings and CRS notices do not block but require explicit Swift confirmation before export. Export streams committed feature rows directly to a temporary file and renames after a successful write/sync. Phase 11 routes layer export and project package share through a shared SwiftUI Export Data pane, then presents the prepared file through the system share sheet. Phase 12 adds scoped KML and CSV + WKT/WKP interchange exports through the same typed service path; CSV can include wkt, wkp, or both geometry columns. GeoJSON remains the highest-fidelity reference data-export path. Phase 13 PDF map layout is a separate Layout output surface, not a data export format.
Newly drawn features receive stable internal UUIDs only when accepted into the edit session. source_feature_id_json remains nil unless a future import/template flow supplies one. Commit recomputes layer bounds, feature counts, geometry types, fields/property values, RTree rows, and dirty state before syncing the manifest. Multipolygon geometries remain supported by storage/import/export, while the Phase 5 Add Part drawing UI is intentionally deferred.
The Rust project ABI returns JSON envelopes with typed error codes. Phase 3 maps errors into recoverable Swift service failures for unsupported file type, invalid package, unsupported project version, invalid GeoJSON, unsupported geometry, coordinate out of range, permission/disk failures, SQLite corruption, query failure, and export failure.
Known limits: the importer performs two streaming passes over the source file, so import time includes a scan pass and a write pass. Each feature is parsed as a GeoJSON value while it is processed, but the full document and full feature set are not retained. Coordinate rounding is intentionally absent in Phase 6; imported numeric coordinate tokens are preserved unless edited. Export progress is currently coarse, with preflight/start/completed surfaced to Swift. Arbitrary PMTiles files may require source-layer metadata or a companion style before they can be displayed beyond the recoverable package import/configuration path.
WKP spike limit: WKP is precision-quantized and cannot preserve arbitrary imported GeoJSON numeric tokens as authoritative storage. It may appear as a scoped CSV geometry field, but it remains out of authoritative storage and standalone layer interchange unless a future container design preserves properties, IDs, CRS warnings, and validation recoverability.