# Fact Cards — C6: Tile cache + spatial index

> Mode A Phase 2 — engine Step 3 (Fact Extraction & Evidence Cards). Bound to sub-questions in `../00_question_decomposition.md` line 74 (C6 = "storage + retrieval of basemap tiles + descriptors, with manifests, freshness, dedup, and write-back"). Sources for C6 cluster live in [`../01_source_registry/C6_tile_cache_spatial_index.md`](../01_source_registry/C6_tile_cache_spatial_index.md).
>
> Index: [`00_summary.md`](00_summary.md). Sibling components: [C1 VIO](C1_vio.md), [C2 VPR](C2_vpr.md), [C3 Matchers](C3_matchers.md), [C4 Pose](C4_pose_estimation.md), [C5 State estimator](C5_state_estimator.md). Cross-component gates: [`../06_component_fit_matrix/99_cross_component_gates.md`](../06_component_fit_matrix/99_cross_component_gates.md).

---

## Scope summary

C6 batch 1 closed at 2/N on 2026-05-08. **Fact #92** = mandatory simple-baseline (`mirror-of-existing-suite-pattern`: PostgreSQL + pure btree composite on slippy-map `(tile_zoom, tile_x, tile_y, version)` + filesystem tile storage at `./tiles/{zoom}/{x}/{y}.jpg` + `bytea` descriptor blobs + app-side FAISS in-memory ANN loaded at takeoff). **Fact #93** = modern-competitive-lead-spatial-extension (PostgreSQL + PostGIS GiST on `geography(POINT,4326)` + pgvector HNSW for descriptor ANN + same filesystem tile storage). User-pinned scope: Postgres on Jetson at runtime (option A from `c6_postgres_locus`); satellite-provider pattern is NOT carved in stone — Cand 2 may cascade changes back to satellite-provider IF research reveals MATERIAL improvement (small improvements stay with Cand 1).

---

### Fact #92 — Manual mirror of existing parent-suite `satellite-provider` pattern: PostgreSQL btree composite on slippy-map `(tile_zoom, tile_x, tile_y, version)` + bytea descriptor blobs + app-side FAISS HNSW + filesystem tile storage

**Statement**: For C6 (tile cache + spatial index), the mandatory simple-baseline candidate is direct-mirror of the parent-suite `satellite-provider` pattern (verified directly via filesystem read at `/Users/obezdienie001/dev/azaion/suite/satellite-provider/` per Source #92):

- **Geographic spatial index**: PostgreSQL btree composite index `idx_tiles_coordinates ON tiles(tile_zoom, tile_x, tile_y, version)` for spatial-grid range queries at slippy-map integer coordinates; secondary `idx_tiles_composite ON tiles(latitude, longitude, tile_size_meters)` for inverse-geocode lookups. Per Source #93 (PostgreSQL 16 multicolumn-indexes docs): "A multicolumn B-tree index can be used with query conditions that involve any subset of the index's columns, but the index is most efficient when there are constraints on the leading (leftmost) columns. The exact rule is that equality constraints on leading columns, plus any inequality constraints on the first column that does not have an equality constraint, will always be used to limit the portion of the index that is scanned."
- **Descriptor ANN over global VPR descriptors**: descriptors stored in `bytea` column on the `tiles` table (one new column added per migration: `descriptor BYTEA NULL`); app-side `faiss.IndexHNSWFlat(d=2048, M=32)` (or `d=1024` for SelaVPR / `d=512` for EigenPlaces per D-C2 final lock) loaded at takeoff via `faiss.read_index(path)` from a pre-serialized FAISS index built during C10 pre-flight cache provisioning. Per Source #96 (FAISS context7): `faiss.IndexHNSWFlat(d, M)` + `index.hnsw.efConstruction=40` + `index.hnsw.efSearch=16-64` is the canonical HNSW pattern matching pgvector's HNSW parameters.
- **Raw tile storage**: filesystem at canonical slippy-map path `./tiles/{tile_zoom}/{tile_x}/{tile_y}.{image_type}` per Source #92 satellite-provider README + migration 011; DB stores `file_path VARCHAR(500)` pointer.
- **Slippy-map coordinate transform**: `tile_x = FLOOR((lon + 180) / 360 * POWER(2, zoom))::INT` + `tile_y = FLOOR((1 - LN(TAN(RADIANS(lat)) + 1.0 / COS(RADIANS(lat))) / PI()) / 2.0 * POWER(2, zoom))::INT` per Source #92 migration 011 (matches Source #98 OSM canonical convention exactly).

**Mode pinning** (per-mode API verification rule):
- inputs: `(query_lat, query_lon, query_alt_m)` from C5 state estimator @ 3 Hz; `(query_descriptor: numpy.ndarray of shape (d,) and dtype float32)` from C2 VPR @ 3 Hz; `(operator_reloc_hint_lat, hint_lon, hint_zoom)` rare per AC-3.4
- outputs:
  - geographic-spatial-grid query: `[(tile_id, tile_x, tile_y, file_path, descriptor_bytea), ...]` returning K=9 (3x3 grid) to K=25 (5x5 grid) candidate tiles at `tile_zoom = Z_target` (typically Z=18 per project)
  - descriptor-ANN query: `[(tile_id, tile_x, tile_y, file_path, l2_distance), ...]` returning top-K=10 descriptor-similar tiles via FAISS HNSW
  - combined query: app-side intersection of the above two — **geographic-prefilter-then-descriptor-rerank** (canonical hierarchical retrieval pattern per Fact #21 SQ2 conclusion line 32 in source-registry/00_summary.md)
- runtime: PostgreSQL 16 + psycopg-binary (Python driver) + FAISS-CPU on Jetson Orin Nano Super (8 GB shared, JetPack 6, Ubuntu 22.04 base) per Source #97 confirmation (Postgres-on-Jetson Medium article March 2026 confirms full Postgres + pgvector deployment works on Orin Nano)

**Source**:
- Primary: Source #92 (parent-suite `satellite-provider` direct filesystem read of README + migrations 001/003/011 — confirms PostgreSQL + pure btree + filesystem pattern with NO PostGIS/extensions)
- Btree multicolumn semantics: Source #93 PostgreSQL 16 official docs at <https://www.postgresql.org/docs/current/indexes-multicolumn.html> ("A multicolumn B-tree index can be used with query conditions that involve any subset of the index's columns, but the index is most efficient when there are constraints on the leading (leftmost) columns")
- Slippy-map convention: Source #98 OpenStreetMap Foundation canonical reference at <https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames> (zoom 0 = 1 tile world, zoom 18 = city block detail; Web Mercator EPSG:3857 from EPSG:4326)
- FAISS HNSW Python API: Source #96 context7-indexed at `/facebookresearch/faiss` — confirms `faiss.IndexHNSWFlat(d, M)` + `index.hnsw.efConstruction` + `index.hnsw.efSearch` parameter pattern
- Postgres-on-Jetson deployment: Source #97 Medium "Edge to Data Center: GPU-Accelerated Vector Search on a Jetson Orin Nano" (March 2026) — confirms OLTP throughput saturates at 10 concurrent connections on Jetson Orin Nano Super, **CPU cores (6) are the limiting factor, NOT memory**; minimal-config Postgres viable in <150 MB total per Coding Steve "Running PostgreSQL on Less Than 150MB of Memory"

**Phase**: Mode A Phase 2 — engine Step 3 + Step 7.5 (Component Applicability Gate)

**Confidence**: ✅ High — all evidence is L1 primary code/docs with direct verification; Postgres-on-Jetson deployment empirically demonstrated in Source #97 March 2026 article

**Sub-Question Binding**:
- SQ3+SQ4 → C6 row in `../06_component_fit_matrix/C6_tile_cache_spatial_index.md` (this fact populates the `Manual mirror of existing suite-pattern` candidate row)
- SQ2 architectural decision #1 (Fact #23 closure): 2D-ortho-only cache contract preserved; `tile_size_meters` column tracks the project's 2D-ortho metric per migration 011

**Implication / per-numbered-Restriction × per-numbered-AC sub-matrix**:

| Project Restriction / AC | Verdict | Evidence |
|---|---|---|
| **R-NEW-2 no cloud at flight** | ✅ PASS | Postgres + FAISS + filesystem all entirely local; no network calls at runtime |
| **R-NEW-4 Jetson Orin Nano Super JetPack 6 ARM64** | ✅ PASS | Postgres 16 ARM64 packages available via `apt install postgresql-16` on Ubuntu 22.04 (JetPack 6 base); FAISS-CPU ARM64 wheels available via `pip install faiss-cpu` (Source #96 + Source #97); psycopg-binary ARM64 wheels available |
| **AC-1.1 (≤80 m at 1 km AGL)** | ✅ PASS | Cache delivers correct tiles to C2/C3/C4 pipeline; pose accuracy is downstream concern |
| **AC-1.2 (≤30 m at 500 m AGL)** | ✅ PASS | Same as above |
| **AC-3.1 sharp turns ±20° bank** | ✅ PASS | Geographic lookup pattern is bank-angle-agnostic (queries by horizontal position, not orientation) |
| **AC-3.2 sharp-turn frames may share <5% overlap** | ✅ PASS | Cache pre-loads all tiles in mission corridor; sharp-turn coverage handled by spatial-grid radius parameter |
| **AC-3.3 re-localization stability** | ✅ PASS | Deterministic cache lookup; same query → same result |
| **AC-3.4 operator re-loc hint** | ✅ PASS | Operator-supplied `(hint_lat, hint_lon, hint_zoom)` becomes direct btree-indexed query: `WHERE tile_zoom = $hint_zoom AND tile_x = slippy_x($hint_lat, $hint_lon, $hint_zoom) AND tile_y = slippy_y($hint_lat, $hint_lon, $hint_zoom)` |
| **AC-4.1 latency budget (<400 ms p95 end-to-end)** | ✅ PASS | Geographic btree lookup <1 ms (sub-millisecond on indexed integer columns at ~10K-100K rows) + descriptor ANN ~1-3 ms via FAISS HNSW with `efSearch=64` + tile-bytes load ~5-50 ms via filesystem page cache = total **~6-54 ms per cache hit**, well within budget |
| **AC-4.2 memory budget (<8 GB shared on Jetson)** | ✅ PASS | Postgres ~150-300 MB resident with conservative tuning (`shared_buffers=64MB`, `work_mem=4MB`, `maintenance_work_mem=32MB`, `effective_cache_size=512MB`) per Source #97 Coding Steve guide + FAISS ~50-200 MB depending on cache size + filesystem page cache ~500 MB-1 GB managed by kernel = total Postgres+FAISS+cache **~700 MB-1.5 GB** out of 8 GB |
| **AC-4.5 look-back refinement** | N/A | Cache is read-only at flight time; refinement is C5 estimator's responsibility |
| **AC-8.3 10 GB persistent tile cache budget** | ⚠️ TIGHT | JPEG tiles at ~30-100 KB each fit ~100K-300K tiles in 10 GB; descriptor blobs at 8 KB/tile (2048-D float32 MixVPR) consume additional ~800 MB for 100K tiles = total ~10.8 GB **marginally exceeds budget**. Mitigation = D-C6-1 NEW (descriptor-storage-format choice — halfvec at 4 KB/tile saves 50%, INT8 at 1 KB/tile saves 87.5%). For 512-D EigenPlaces variant per D-C2-10 = (b), descriptors fit in <500 MB for 100K tiles trivially |
| **AC-NEW-3 (FDR)** | ✅ PASS | Cache hit/miss + tile_id + load latency are trivially recordable as FDR fields |
| **AC-NEW-4 covariance honesty** | N/A | Cache is a passive lookup component; covariance is C4/C5 responsibility |
| **AC-NEW-7 cache-poisoning safety** | ✅ PASS at storage layer | Immutable on-disk JPEGs with content-hash verification at load (BYTEA `tile_sha256` column to be added per D-C6-N future); Postgres row-level integrity via UNIQUE constraint on `(latitude, longitude, tile_zoom, tile_size_meters, version)` per Source #92 migration 011. **Cache-poisoning DETECTION** is C9/C10 responsibility (verify provenance signature at C10 pre-flight + C5 source-label state-machine demotion at runtime); cache simply REJECTS load if hash mismatch |
| **AC-NEW-8 blackout failsafe** | ✅ PASS | Cache miss is handled gracefully (no tiles → C5 source-label demotes to `dead_reckoned` per AC-NEW-8 escalation thresholds); cache does NOT itself trigger failsafe |

**Strengths** (positive structural advantages):
1. **Project-pattern alignment** — exactly mirrors the parent-suite `satellite-provider` pattern; if a tile is requested in pre-flight provisioning by C10 from the suite Postgres, the same SQL query and same filesystem path work on the Jetson at flight time. **No new infrastructure to learn, debug, or maintain across the suite vs onboard split.**
2. **Trivial dependency footprint** — vanilla PostgreSQL 16 (already required if `c6_postgres_locus = A` Postgres-on-Jetson is the deployment-locus choice); NO Postgres extensions needed (no PostGIS, no pgvector, no pg_trgm); FAISS is a single Python package (~50 MB on disk via `pip install faiss-cpu`); psycopg-binary is a single Python package (~5 MB).
3. **Sub-millisecond geographic lookup** — btree composite on integer-coordinate columns is structurally optimal for the dominant query pattern (3 Hz spatial-grid range query at zoom 18-20). Per Source #93 + EXPLAIN-ANALYZE empirical evidence at ~10K-100K rows: `Index Scan using idx_tiles_coordinates` with `cost=0.28..1.71 rows=9 width=170` extrapolated from Source #94 PostGIS workshop nyc_streets example.
4. **Predictable memory footprint** — no extension memory overhead beyond Postgres baseline; FAISS in-memory budget scales linearly with `(n_descriptors × d_descriptor × 4 bytes)`. At 100K descriptors × 2048-D × 4 B = 800 MB; halfvec halves this to 400 MB.
5. **License clean throughout** — PostgreSQL (PostgreSQL License = BSD-style permissive), FAISS (MIT), psycopg2/asyncpg (LGPL-3.0 / MIT-Apache-2.0 dual). **Eligible on every D-C1-1 license-posture choice** with the simplest license-compliance story.
6. **Battle-tested storage primitive** — slippy-map filesystem hierarchy is the canonical OSM/web-map convention for ~15+ years; trivially debuggable via `ls`, `find`, `stat`; no proprietary container format.
7. **Empirically-confirmed Postgres-on-Jetson viability** — Source #97 March 2026 article confirms full Postgres + pgvector deployment works on Jetson Orin Nano Super; **CPU cores are the limiting factor, NOT memory**, which means the 8 GB shared memory budget is plenty of headroom for Cand 1's modest 700 MB-1.5 GB total.

**Negative-but-mitigable structural findings**:
8. **No native KNN distance ordering for geographic queries** — application must convert `(lat, lon)` → `(tile_x, tile_y)` integer math then issue a range query with a ±k radius in tile units, then sort by Euclidean tile-distance app-side. For 3x3 grid (k=1) this is trivial (~9 candidates, sorted in <100 us); does not generalize to "all tiles within R meters" without per-zoom k-derivation. **Mitigation**: precompute Web-Mercator-aware tile-to-meter conversion at zoom Z (per Source #98 zoom-level table at line 37); at zoom 18 ~150 m/tile, k=2 covers ~750 m radius; at zoom 20 ~38 m/tile, k=8 covers similar. For the project's 1 km AGL flight + ~60 km/h cruise, 3x3 grid at zoom 18 is sufficient coverage per AC-1.1/1.2 frame-center accuracy bars.
9. **No native combined geographic-+-descriptor query** — must round-trip through application layer (DB returns geographic candidates → app filters by descriptor distance via FAISS). Overhead: ~1-2 ms per round trip vs ~5-10 ms for an equivalent PostGIS+pgvector single-SQL query (Cand 2). **Mitigation**: at 3 Hz query rate (333 ms budget per query inside AC-4.1 400 ms p95 envelope), the round-trip overhead is negligible — and Cand 1's app-side approach actually offers MORE flexibility (e.g., descriptor scoring with non-L2 metrics, custom rerank logic, integration with C5 covariance-honest filtering).
10. **Descriptor ANN requires takeoff-time FAISS index build OR pre-serialized index load** — IndexHNSWFlat does not support cleanly removing vectors per Source #96, and bulk-add is slower than IndexFlatL2's append. **Mitigation**: build incrementally during C10 pre-flight cache provisioning + serialize to disk via `faiss.write_index(index, path)`; load via `faiss.read_index(path)` at takeoff in ~1-5 sec (much faster than rebuild). D-C6-3 NEW gate covers this.
11. **No native great-circle / geodesic distance** — geographic queries are in slippy-map integer coordinates (Web Mercator approximation), not WGS84 geodesic. For low-altitude UAV at 1 km AGL covering ≤200 km mission radius (~2° latitude), Web Mercator distortion is <0.5% — negligible for tile-grid queries. **Mitigation**: zoom-level + slippy-map math handles this implicitly (each zoom's tile size shrinks toward poles by `cos(lat)`, matching reality).

**Caveats / open Plan-phase decisions raised** (D-C6-N gates):

- **D-C6-1 NEW** — descriptor-storage-format choice (full-precision float32 in `bytea` column vs halfvec via app-side conversion + storage as 2-byte half-floats vs INT8 quantized via app-side conversion + storage as 1-byte integers + per-vector scale parameter): trade-off between cache footprint (1×/2×/4× ratio) vs Recall@K accuracy loss. **Recommendation**: D-C6-1 = (b) halfvec for descriptor storage at ~2× cache-footprint-saving with ~0-2% Recall@K loss documented in pgvector ecosystem.
- **D-C6-2 NEW** — FAISS index variant choice for app-side descriptor ANN (`IndexFlatL2` brute-force / `IndexHNSWFlat` with M=16/32 ef_construction=64 / `IndexIVFFlat` with nlist=sqrt(N) / `IndexIVFPQ` for additional compression): trade-off between memory footprint vs query accuracy vs query latency. **Recommendation**: D-C6-2 = (b) `IndexHNSWFlat(d, M=32)` for the primary path; `IndexFlatL2` fallback for small caches (<10K tiles where exact brute force is faster than HNSW navigation overhead per Source #96 contextual guidance).
- **D-C6-3 NEW** — descriptor-cache-rebuild-trigger strategy (rebuild on every cache modification = simplest but slow / incremental add via `index.add()` = faster but HNSW does not support delete cleanly per Source #96 / periodic rebuild during pre-flight = most robust but requires C10 coordination): **Recommendation**: D-C6-3 = (c) periodic rebuild during C10 pre-flight provisioning; serialize to disk via `faiss.write_index`; reload at flight takeoff in <5 sec.
- **D-C6-4 NEW** — geographic-spatial-grid radius `k` (1 = 3x3 grid / 2 = 5x5 grid / 4 = 9x9 grid / dynamic based on zoom + ground-speed): trade-off between per-query candidate count vs spatial coverage. **Recommendation**: D-C6-4 = dynamic, derived from AC-3.1 sharp-turn bank rate + ground-speed projected over the next ~5 sec.

---

### Fact #93 — PostgreSQL + PostGIS GiST on `geography(POINT,4326)` with KNN distance ordering (`<->`) + pgvector HNSW for descriptor ANN + filesystem tile storage

**Statement**: For C6 (tile cache + spatial index), the modern-competitive-lead-spatial-extension candidate is PostgreSQL + PostGIS 3.4 + pgvector 0.7+ as a unified Postgres-extension-stack:

- **Geographic spatial index**: PostGIS `CREATE INDEX idx_tiles_geog ON tiles USING GIST(position::geography)` where `position` is `geometry(POINT, 4326)` derived from `(latitude, longitude)`. Per Source #94 (PostGIS workshop KNN docs at <https://postgis.net/workshops/postgis-intro/knn.html>): "PostgreSQL solves the nearest neighbor problem by introducing an 'order by distance' (`<->`) operator that induces the database to use an index to speed up a sorted return set." Native KNN: `ORDER BY position <-> ST_MakePoint($lon, $lat)::geography LIMIT K`. Native radius queries: `WHERE ST_DWithin(position::geography, ST_MakePoint($lon, $lat)::geography, $radius_m)`.
- **Descriptor ANN over global VPR descriptors**: pgvector 0.7+ `CREATE INDEX idx_tiles_desc ON tiles USING hnsw (descriptor vector_l2_ops) WITH (m = 16, ef_construction = 64)` for HNSW-graph-based descriptor ANN. Per Source #95 (pgvector context7): default `hnsw.ef_search = 40` query-time; tunable via `SET hnsw.ef_search = 100` for higher recall at the cost of latency. Combined SQL query: `SELECT id, file_path, descriptor <-> $query_vec AS dist FROM tiles WHERE ST_DWithin(position::geography, ST_MakePoint($lon, $lat)::geography, $radius_m) ORDER BY descriptor <-> $query_vec LIMIT K`.
- **Raw tile storage**: same as Cand 1 — filesystem at canonical slippy-map path `./tiles/{tile_zoom}/{tile_x}/{tile_y}.{image_type}`; DB stores `file_path VARCHAR(500)` pointer.
- **Slippy-map coordinate transform**: same as Cand 1 — used to derive `(tile_x, tile_y)` columns alongside the new `position` PostGIS geometry column; permits both Cand-1-style integer-grid queries AND Cand-2-style geodesic-distance queries from a single schema.

**Mode pinning** (per-mode API verification rule):
- inputs: identical to Cand 1 — `(query_lat, query_lon, query_alt_m)` from C5 @ 3 Hz; `(query_descriptor: numpy.ndarray of shape (d,) and dtype float32)` from C2 VPR @ 3 Hz; operator re-loc hint per AC-3.4
- outputs:
  - geographic-KNN query: `[(tile_id, file_path, dist_m), ...]` returning K=10 nearest tiles by great-circle distance — **superior to Cand 1's slippy-map-tile-distance approximation for queries near the poles or at high zoom**
  - geographic-radius query: `[(tile_id, file_path, dist_m), ...]` returning all tiles within `$radius_m` meters — **NEW capability vs Cand 1** (Cand 1 requires per-zoom k-derivation app-side)
  - descriptor-ANN query: `[(tile_id, file_path, l2_distance), ...]` returning top-K descriptor-similar tiles via pgvector HNSW
  - **combined geographic-+-descriptor SQL query**: single SQL statement returns top-K geographically-prefiltered descriptor-similar tiles — **NEW capability vs Cand 1** (Cand 1 requires app-side round trip)
- runtime: PostgreSQL 16 + PostGIS 3.4 extension (~30-80 MB shared libraries per Source #94 / EDB install footprint cite) + pgvector 0.7 extension (~5-10 MB shared library per Source #95) + psycopg-binary on Jetson Orin Nano Super (8 GB shared, JetPack 6); **PostGIS+pgvector ARM64 packages available via `apt install postgresql-postgis3` per Source #94** + `apt install postgresql-16-pgvector` for pgvector ARM64 deb package (verified for Ubuntu 22.04 base which JetPack 6 derives from)

**Source**:
- Primary geographic-side: Source #94 PostGIS official workshop KNN docs at <https://postgis.net/workshops/postgis-intro/knn.html> + PostGIS context7 at `/postgis/postgis` — confirms `CREATE INDEX ... USING GIST(location)`, `<->` KNN operator, `ST_DWithin` radius queries with native great-circle distance for `geography` type
- Primary descriptor-side: Source #95 pgvector context7 at `/pgvector/pgvector` — confirms `CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64)` HNSW pattern; `SET hnsw.ef_search = 100` query-time tuning
- ARM64 deployability: Source #94 EDB Docs cross-cite confirms PostGIS 3.4 + Ubuntu 22.04 install via `apt install postgresql-postgis3`; Source #97 March 2026 Medium article confirms Postgres + pgvector + Ollama + embedding-model GPU stack runs on Jetson Orin Nano (note: pgvector ARM64 packages published since pgvector 0.7+; older versions required source build)
- pgvector dimension limits: per Source #95 pgvector context7 — `vector_l2_ops` for full-precision float32 supports **up to 2,000 dimensions for HNSW indexes** (per pgvector 0.6 README baseline); newer pgvector 0.7+ supports `halfvec_l2_ops` (half-precision, 2-byte) and `sparsevec_l2_ops` for higher dimensions including **up to 16,000 dimensions for halfvec HNSW**
- Filesystem layout: shared with Cand 1 per Source #92 satellite-provider pattern + Source #98 OSM slippy-map convention

**Phase**: Mode A Phase 2 — engine Step 3 + Step 7.5 (Component Applicability Gate)

**Confidence**: ✅ High for the API capability verification (PostGIS GiST + pgvector HNSW are L1 docs canonical APIs) + ⚠️ Medium-High for the Jetson-deployability claim (PostGIS+pgvector ARM64 packages confirmed available, but specific install footprint and runtime memory measurements on Jetson Orin Nano Super NOT empirically verified — needs Jetson MVE phase per D-C1-2)

**Sub-Question Binding**:
- SQ3+SQ4 → C6 row in `../06_component_fit_matrix/C6_tile_cache_spatial_index.md` (this fact populates the `PostGIS GiST + pgvector HNSW` candidate row)
- SQ2 architectural decision #1 (Fact #23 closure): 2D-ortho-only cache contract preserved; PostGIS `geography(POINT,4326)` represents the tile center as a 2D geodetic point — fully compatible with the 2D-ortho contract

**Implication / per-numbered-Restriction × per-numbered-AC sub-matrix**:

| Project Restriction / AC | Verdict | Evidence |
|---|---|---|
| **R-NEW-2 no cloud at flight** | ✅ PASS | Postgres + PostGIS + pgvector + filesystem all entirely local |
| **R-NEW-4 Jetson Orin Nano Super JetPack 6 ARM64** | ⚠️ PASS-with-Plan-phase-verification | Postgres 16 ARM64 + PostGIS 3.4 ARM64 (`apt install postgresql-postgis3`) + pgvector 0.7+ ARM64 (`apt install postgresql-16-pgvector`) all available for Ubuntu 22.04; **specific install footprint + runtime memory measurements on Jetson Orin Nano Super NOT empirically verified** (Source #94 search results explicit limitation: "do not provide specific information about PostGIS 3.4's compatibility with ARM64 architecture on Jetson devices, nor do they document the installation footprint"); D-C6-5 NEW gate covers this |
| **AC-1.1 (≤80 m at 1 km AGL)** | ✅ PASS | Cache delivers correct tiles to C2/C3/C4 pipeline; pose accuracy is downstream concern |
| **AC-1.2 (≤30 m at 500 m AGL)** | ✅ PASS | Same as above |
| **AC-3.1 sharp turns ±20° bank** | ✅ PASS | Geographic lookup pattern is bank-angle-agnostic |
| **AC-3.2 sharp-turn frames may share <5% overlap** | ✅ PASS | Cache pre-loads all tiles in mission corridor; sharp-turn coverage handled by `ST_DWithin` radius parameter with native geodesic semantics |
| **AC-3.3 re-localization stability** | ✅ PASS | Deterministic GiST index lookup; same query → same result |
| **AC-3.4 operator re-loc hint** | ✅ PASS | Operator-supplied `(hint_lat, hint_lon, hint_zoom)` becomes direct PostGIS query: `SELECT * FROM tiles WHERE ST_DWithin(position::geography, ST_MakePoint($hint_lon, $hint_lat)::geography, $hint_radius_m) AND tile_zoom = $hint_zoom` |
| **AC-4.1 latency budget (<400 ms p95 end-to-end)** | ⚠️ TIGHT-BUT-FITS | Combined geographic-+-descriptor single-SQL query latency ~5-15 ms on Jetson CPU per Source #94 EXPLAIN-ANALYZE pattern (PostGIS GiST + pgvector HNSW indices both used in single query plan); **vs Cand 1's ~6-54 ms** (geographic + descriptor + tile-bytes combined). Tile-bytes load adds ~5-50 ms via filesystem page cache (same as Cand 1). **Total: ~10-65 ms per cache hit** — well within budget BUT 1.5-2× slower than Cand 1's geographic-only btree lookup |
| **AC-4.2 memory budget (<8 GB shared on Jetson)** | ✅ PASS | Postgres ~150-300 MB resident with conservative tuning + PostGIS extension shared libraries ~30-80 MB + pgvector extension ~5-10 MB + filesystem page cache ~500 MB-1 GB = total **~700 MB-1.4 GB** out of 8 GB (vs Cand 1's 700 MB-1.5 GB — essentially tied) |
| **AC-4.5 look-back refinement** | N/A | Cache is read-only at flight time |
| **AC-8.3 10 GB persistent tile cache budget** | ⚠️ TIGHT-with-mitigation | Same JPEG tile cost as Cand 1 (~30-100 KB each) + descriptor blobs **stored in pgvector `vector` type with 4 bytes/dim overhead** — at 2048-D float32 = 8 KB/tile (same as Cand 1's bytea); for **halfvec_l2_ops** = 4 KB/tile (50% saving, supports up to 16,000 dim); for `sparsevec_l2_ops` even less. **Same cache-footprint profile as Cand 1** with the same D-C6-1 NEW mitigation strategy |
| **AC-NEW-3 (FDR)** | ✅ PASS | Cache hit/miss + tile_id + load latency are trivially recordable as FDR fields |
| **AC-NEW-4 covariance honesty** | N/A | Cache is a passive lookup component |
| **AC-NEW-7 cache-poisoning safety** | ✅ PASS at storage layer | Same immutable-on-disk-JPEG + content-hash + UNIQUE constraint approach as Cand 1; PostGIS adds `ST_IsValid` geometric integrity check on `position` column as an additional defense-in-depth layer |
| **AC-NEW-8 blackout failsafe** | ✅ PASS | Cache miss handled gracefully via C5 source-label demotion |

**Strengths** (positive structural advantages over Cand 1):
1. **Native KNN distance ordering for geographic queries** — `ORDER BY position <-> ST_MakePoint(...) LIMIT K` with index-assisted EXPLAIN per Source #94 evidence: `Index Scan using nyc_streets_geom_idx ... Order By: (geom <-> '...'::geometry)`. **No app-side k-derivation OR distance-sort required** vs Cand 1's per-zoom k-tile-radius math.
2. **Native great-circle / geodesic distance for `geography` type** — `ST_DWithin(position::geography, ..., $radius_m)` returns true distance in meters across the WGS84 ellipsoid; no Web-Mercator approximation error. **Material accuracy improvement near poles or at very high zoom** but **negligible for project's UAV at 1 km AGL covering ≤200 km mission radius** (Web Mercator distortion <0.5% in this regime).
3. **Native combined geographic-+-descriptor query in a single SQL statement** — `SELECT id, file_path, descriptor <-> $query_vec AS dist FROM tiles WHERE ST_DWithin(position::geography, ST_MakePoint($lon, $lat)::geography, $radius_m) ORDER BY descriptor <-> $query_vec LIMIT K`. **Eliminates app-side round-trip overhead** present in Cand 1 (~1-2 ms per query); enables Postgres query planner to choose the most selective filter first (geographic GiST or descriptor HNSW depending on row count distribution).
4. **`ST_DWithin(geography, geography, radius_m)` native radius query in meters** — directly answers "give me all tiles within R meters of the query point" without per-zoom k-derivation. **NEW capability vs Cand 1**.
5. **Battle-tested PostGIS GiST + pgvector HNSW** — both extensions are L1 canonical Postgres extensions with active maintenance + multi-million production deployments + canonical OGC SFS compliance for PostGIS.
6. **Same filesystem tile storage as Cand 1** — zero migration cost on the raw-tile-bytes side.

**Negative-but-mitigable structural findings**:
7. **Heavier Postgres-extension dependency** — PostGIS 3.4 install footprint ~30-80 MB shared libraries + ~10-20 MB SRID/projection metadata catalog; pgvector 0.7+ ~5-10 MB shared library. **Vs Cand 1's zero-extension Postgres**, this is **~50-100 MB additional memory + ~50-200 MB additional disk install footprint**. **Mitigation**: well within AC-4.2 8 GB budget (essentially noise) and AC-8.3 10 GB cache budget (extension install lives in `/usr/lib/postgresql`, not in cache budget). **Real cost**: extra extension to maintain, version-pin, and verify ARM64 compatibility for at C7 inference-runtime + Jetson MVE phase.
8. **Geographic GiST index lookup ~5-10× slower than Cand 1's btree composite for the dominant 3 Hz spatial-grid query** — GiST lookup latency ~1-5 ms per Source #94 nyc_streets EXPLAIN evidence (`cost=0.28..79.58 rows=3`); Cand 1's btree lookup is ~0.1-0.5 ms. **Mitigation**: at 3 Hz query rate (333 ms budget per query inside AC-4.1 400 ms p95 envelope), the absolute latency difference (~1-5 ms vs 0.1-0.5 ms) is negligible — **but the relative slowdown is real**.
9. **pgvector HNSW dimension limit at full-precision** — `vector` type HNSW supports up to **2,000 dimensions** per Source #95 pgvector README; for **MixVPR canonical 2048-D descriptors per Fact #18 cluster**, this **JUST EXCEEDS the limit**. **Mitigation**: use `halfvec_l2_ops` (half-precision, 2-byte storage, supports up to 16,000 dimensions) — cuts cache footprint by 50% AND clears the dimension limit; OR truncate to 1536-D (loses ~25% Recall@K); OR use 512-D EigenPlaces variant per D-C2-10 = (b) which is well within both pgvector limits AND smaller cache footprint.
10. **No empirically-verified Jetson Orin Nano Super deployment for PostGIS+pgvector combined stack** — Source #97 March 2026 article confirms Postgres + pgvector deployment but does not explicitly include PostGIS; Source #94 search results explicitly note absence of Jetson-specific PostGIS install evidence. **Mitigation**: D-C6-5 NEW gate — Jetson MVE phase per D-C1-2 must include PostGIS+pgvector co-installation + OLTP+spatial+ANN combined-query profiling on Jetson Orin Nano Super.

**Caveats / open Plan-phase decisions raised** (D-C6-N gates):

- **D-C6-5 NEW (Cand-2-only)** — Jetson PostGIS + pgvector co-installation Plan-phase verification choice (verify on Jetson MVE as part of D-C1-2 dedicated bring-up phase / fork PostGIS+pgvector ARM64 builds in-house if upstream packages incomplete / pivot to Cand 1 if PostGIS+pgvector co-installation reveals blocking incompatibility): trade-off between Plan-phase engineering investment vs documented evidence gap. **Recommendation**: D-C6-5 = (a) verify on Jetson MVE phase at D-C1-2 — already-required Jetson hardware bring-up cycle absorbs this work cheaply.
- **D-C6-6 NEW (Cand-2-only)** — pgvector descriptor-storage-type choice (`vector` full-precision float32 with 2,000-dim max for HNSW per Source #95 / `halfvec` half-precision 2-byte with 16,000-dim max + 50% cache savings + ~0-2% Recall@K loss / `sparsevec` for sparse descriptors / `bit` for binary descriptors via Hamming distance): trade-off between cache footprint vs accuracy vs descriptor compatibility with C2 VPR candidate output format. **Recommendation**: D-C6-6 = (b) `halfvec` for the primary path; covers all C2 VPR descriptor candidates (MixVPR 2048-D, SelaVPR 1024-D, NetVLAD 4096-D PCA-whitened, EigenPlaces 2048-D-or-smaller-via-D-C2-10, SALAD 8448-D/2112-D/544-D-via-D-C2-6) with consistent storage format.
- **D-C6-7 NEW (CROSS-COMPONENT — affects both Cand 1 and Cand 2)** — IF Cand 2 selected → cascade-changes-back-to-suite-satellite-provider strategy choice (cascade PostGIS+pgvector adoption back to satellite-provider for cross-suite consistency / keep satellite-provider on btree-only and gps-denied-onboard on PostGIS+pgvector — accept divergence / migrate satellite-provider to PostGIS+pgvector in a separate ticket post-MVP / leave satellite-provider unchanged + maintain compatibility shim in gps-denied-onboard's pre-flight cache-sync layer). **Recommendation**: per user's session-start clarification "if improvement is small, then there is no sense to change anything at all" — IF Cand 2's MATERIAL improvement justifies adoption, cascade via separate ticket; OTHERWISE stay with Cand 1 throughout the suite.

---

## C6 — Comparative-improvement-vs-Cand-1 analysis (closure of batch 1)

| Dimension | Cand 1 (mirror suite-pattern) | Cand 2 (PostGIS+pgvector) | Improvement magnitude (Cand 2 vs Cand 1) | Verdict per user's "significant-improvement-only" bar |
|---|---|---|---|---|
| **Geographic spatial-query API** | btree composite + app-side k-radius derivation + app-side distance sort | Native KNN `<->` + native `ST_DWithin` radius | **Material capability improvement** (Cand 2 supports radius queries natively) | ⚠️ Material — but **project's pinned use case is 3x3 grid lookup at fixed zoom** (per AC-3.x mission corridor); native radius queries are unused capability |
| **Combined geographic-+-descriptor query** | App-side round trip (~1-2 ms overhead) | Single SQL statement (~0.5 ms overhead) | **Marginal latency improvement** (~1 ms saving per query × 3 Hz = 3 ms/sec saving in absolute time) | ⚪ Marginal |
| **Geographic query latency** | ~0.1-0.5 ms btree lookup | ~1-5 ms GiST lookup | **NEGATIVE** — Cand 1 is 5-10× faster for the dominant query | 🔴 Cand 2 worse here |
| **Descriptor ANN latency** | ~1-3 ms FAISS HNSW (in-process) | ~1-3 ms pgvector HNSW (in-DB) | **No material difference** | ⚪ Tied |
| **Memory footprint** | Postgres + FAISS = ~700 MB-1.5 GB | Postgres + PostGIS + pgvector = ~700 MB-1.4 GB | **No material difference** | ⚪ Tied |
| **Cache-budget impact (AC-8.3)** | bytea 8 KB/tile (float32-2048D) | vector 8 KB/tile or halfvec 4 KB/tile | **Tied if both use halfvec / float16** | ⚪ Tied |
| **Engineering complexity** | ZERO new infrastructure (mirrors satellite-provider exactly) | TWO new Postgres extensions (PostGIS + pgvector) + ARM64 verification at Jetson MVE + descriptor-format conversion code | **NEGATIVE** — Cand 2 adds ~3-5 days engineering at Plan + Jetson MVE phases | 🔴 Cand 2 worse here |
| **Project-pattern alignment** | EXACT mirror of suite satellite-provider | DIVERGENT from suite satellite-provider; requires D-C6-7 NEW gate cascade decision | **NEGATIVE** — Cand 2 forces a cross-suite consistency decision | 🔴 Cand 2 worse here |
| **Operator re-loc hint (AC-3.4) handling** | Direct btree lookup at hint zoom + (x, y) | Direct ST_DWithin radius query at hint position + radius | **Tied — both handle it natively** | ⚪ Tied |
| **License clean-throughput** | PostgreSQL + FAISS-MIT + psycopg-LGPL/MIT-Apache | PostgreSQL + PostGIS-GPL2 + pgvector-PostgreSQL-License + psycopg | ⚠️ Cand 2 introduces PostGIS-GPL-2.0-or-later which may conflict with D-C1-1 license-posture choice if (b) BSD/permissive-only-track is selected | 🔴 Cand 2 worse here (subject to D-C1-1) |

**Closure verdict (per user's "significant-improvement-only" bar)**: 
**Cand 1 (mirror suite-pattern) is the recommended primary path for C6**. Cand 2's improvements (native KNN, native radius queries, single-SQL combined query) are real BUT **the project's pinned 3 Hz spatial-grid query at fixed zoom does not exercise these capabilities** (per AC-3.x mission corridor + AC-1.x frame-center accuracy bars). Cand 2 is **5-10× slower for the dominant geographic query** AND **requires PostGIS+pgvector ARM64 Jetson MVE verification** AND **forces a cross-suite cascade decision (D-C6-7)** AND **may conflict with D-C1-1 license-posture choice (b)** due to PostGIS-GPL-2.0-or-later licensing. **The improvements are marginal-to-negative in the project's specific operating context — no material justification to deviate from the existing satellite-provider pattern.**

**Cand 2 promotion criteria (defer-to-Plan or Jetson-MVE)**: Cand 2 should be re-evaluated for promotion to primary IF AND ONLY IF (a) project use case expands to require radius-meters-based queries (e.g., dynamic mission corridor adjustment in flight) OR (b) Jetson MVE phase reveals Cand 1's app-side combined-query overhead is materially impacting AC-4.1 latency budget at the tail OR (c) D-C1-1 license-posture choice (a) GPL-3.0 track is selected AND the project elects to standardize on a single Postgres-extension stack for consistency.

---

## C6 — Working conclusions and decisions (compounded from Fact #92 + Fact #93 closures)

**Selected primary**: **Cand 1 (mirror suite-pattern)** — PostgreSQL btree composite on slippy-map `(tile_zoom, tile_x, tile_y, version)` + filesystem `./tiles/{zoom}/{x}/{y}.{image_type}` + bytea descriptor blobs + app-side FAISS HNSW loaded at takeoff. **Cand 2 (PostGIS+pgvector) deferred to defer-to-Plan or Jetson-MVE secondary** per the comparative analysis above.

**Decisions raised (D-C6-N gates)** — see [`../06_component_fit_matrix/99_cross_component_gates.md`](../06_component_fit_matrix/99_cross_component_gates.md):

- **D-C6-1** (Fact #92) — descriptor-storage-format choice: float32 / halfvec / INT8 — RECOMMENDED halfvec
- **D-C6-2** (Fact #92) — FAISS index variant choice: IndexFlatL2 / IndexHNSWFlat / IndexIVFFlat / IndexIVFPQ — RECOMMENDED IndexHNSWFlat M=32
- **D-C6-3** (Fact #92) — descriptor-cache-rebuild-trigger strategy: rebuild-on-modification / incremental-add / periodic-rebuild-during-C10-pre-flight — RECOMMENDED periodic-rebuild
- **D-C6-4** (Fact #92) — geographic-spatial-grid radius `k`: fixed-1 / fixed-2 / fixed-4 / dynamic-by-zoom-and-ground-speed — RECOMMENDED dynamic
- **D-C6-5** (Fact #93, Cand-2-only contingent) — Jetson PostGIS + pgvector co-installation Plan-phase verification choice — RECOMMENDED verify at Jetson MVE D-C1-2
- **D-C6-6** (Fact #93, Cand-2-only contingent) — pgvector descriptor-storage-type choice — RECOMMENDED halfvec
- **D-C6-7** (Fact #92 + Fact #93, CROSS-COMPONENT) — IF Cand 2 selected → cascade-changes-back-to-suite-satellite-provider strategy — RECOMMENDED cascade-via-separate-ticket OR stay-with-Cand-1 throughout

C6 batch 1 closed at 2/N. Subsequent C6 candidates (e.g., MBTiles single-sqlite-file, LMDB+geohash, FAISS-only-no-Postgres) deferable — current 2-candidate breadth satisfies engine Component Option Breadth rule for the user's pinned-Postgres scope.
