# C6 — Tile Cache + Spatial Index

## 1. High-Level Overview

**Purpose**: own the on-companion persistent imagery store (tiles + descriptor index + tile metadata) in a layout that is byte-identical to `satellite-provider`'s on-disk format, so the C11 `TileUploader` post-landing upload (F10) is a straight copy. Provide read access to C2 (descriptor index), C2.5 / C3 (tile pixels), and write access to the orthorectifier path during F4 (mid-flight tile generation), to the C11 `TileDownloader` during F1 (pre-flight tile fetch), and to C10 during F1 (Manifest + FAISS index write).

**Architectural Pattern**: Repository — three concrete stores behind separate interfaces (`TileStore` for pixel + metadata I/O; `TileMetadataStore` for the Postgres spatial index; `DescriptorIndex` for FAISS HNSW). Single concrete implementation per interface today (`PostgresFilesystemStore`, `FaissDescriptorIndex`); future variants (e.g., RocksDB-backed metadata for resource-constrained tiers) can be added behind the same interfaces.

**Upstream dependencies**:
- C11 `TileDownloader` (writes `tiles` rows + JPEGs during F1 pre-flight provisioning, source='googlemaps').
- C10 CacheProvisioner (writes Manifest + FAISS index during F1 pre-flight provisioning, after C11 has populated tiles).
- C5 → orthorectifier → C6 (writes during F4 mid-flight tile generation, source='onboard_ingest').
- C11 `TileUploader` (reads during F10).

**Downstream consumers**:
- C2 VPR (reads descriptor index).
- C2.5 ReRanker (reads tile pixels).
- C3 CrossDomainMatcher (reads tile pixels).

## 2. Internal Interfaces

### Interface: `TileStore`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `read_tile_pixels` | `tile_id: TileId` | `TilePixelHandle` (mmap-backed) | No | `TileNotFoundError`, `TileFsError` |
| `write_tile` | `tile_blob: bytes, metadata: TileMetadata` | `None` | No | `TileFsError`, `TileMetadataError` |
| `tile_exists` | `tile_id: TileId` | `bool` | No | — |

### Interface: `TileMetadataStore`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `query_by_bbox` | `bbox, zoom` | `list[TileMetadata]` | No | `TileMetadataError` |
| `insert_metadata` | `TileMetadata` | `None` | No | `TileMetadataError` |
| `mark_voting_status` | `tile_id, status: VotingStatus` | `None` | No | `TileMetadataError` |

### Interface: `DescriptorIndex`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `search_topk` | `query: ndarray[D], k: int` | `list[(tile_id, distance)]` | No | `IndexUnavailableError` |
| `descriptor_dim` | `()` | `int` | No | — |
| `mmap_handle` | `()` | filesystem path / FAISS reader handle | No | — |

**Input/Output DTOs**:
```
TileId:                       composite (zoomLevel: int, lat: float, lon: float)

TileMetadata:
  tile_id:                       TileId
  tile_size_meters:              float
  tile_size_pixels:              int
  capture_timestamp:             ISO 8601 datetime
  source:                        enum {googlemaps, onboard_ingest}
  freshness_label:               enum {fresh, stale_active_conflict, stale_rear, downgraded}
  flight_id:                     uuid (optional, set for onboard_ingest)
  companion_id:                  string (optional, set for onboard_ingest)
  quality_metadata:              TileQualityMetadata (optional, set for onboard_ingest)
  voting_status:                 enum {pending, trusted, rejected} (default pending for onboard_ingest)

TileQualityMetadata:           see data_model.md (TileQualityMetadata entity)

TilePixelHandle:               opaque (filesystem path + mmap pointer; consumer must not copy)
```

## 3. External API Specification

Not applicable — internal-only; C11 `TileUploader` reads via `TileStore` for upload to `satellite-provider` over an external HTTP API, and C11 `TileDownloader` writes via `TileStore` after fetching from `satellite-provider`. C11 owns those API calls, not C6.

## 4. Data Access Patterns

### Queries

| Query | Frequency | Hot Path | Index Needed |
|-------|-----------|----------|--------------|
| `read_tile_pixels` (3 candidates per frame from C2.5/C3) | 9 Hz | Yes | Filesystem mmap (page-cache backed) |
| `search_topk` from C2 (top-K=10) | 3 Hz | Yes | FAISS HNSW (.index file) |
| `query_by_bbox` from C10/C11/C12 (cache build + post-landing) | offline / pre-flight | No | btree spatial index on (zoomLevel, lat, lon) |
| `insert_metadata` + `write_tile` from F4 orthorectifier | bursty during flight (≤ a few Hz on average) | No (background) | btree spatial index |

### Caching Strategy

| Data | Cache Type | TTL | Invalidation |
|------|-----------|-----|-------------|
| Tile pixels | OS page cache (filesystem mmap) | flight lifetime | None — files are append-only during flight; F1 wipes for next flight |
| FAISS descriptor index | mmap (`IO_FLAG_MMAP_IFC`) | flight lifetime | F1 rebuild + content-hash gate |
| Tile metadata rows | Postgres internal page cache + connection pool | flight lifetime | F4 inserts append; F1 may truncate-and-reload |

### Storage Estimates

| Table/Collection | Est. Row Count (1yr) | Row Size | Total Size | Growth Rate |
|-----------------|---------------------|----------|------------|-------------|
| `tiles` (Postgres rows) | ~ tens to hundreds of thousands per cached area | ~256 B per row | up to ~100 MB | bounded by 10 GB AC-8.3 cache budget overall |
| `tiles/{zoomLevel}/{x}/{y}.jpg` | matches `tiles` row count | 50–200 KB / tile | up to ~9 GB (after metadata/index overheads) | bounded by AC-8.3 |
| FAISS HNSW `.index` | one file per provisioning | 200 MB–1 GB depending on backbone descriptor dim | bounded by AC-8.3 carve-out per D-C2-6/9/10 | F1 atomic rebuild |
| Onboard mid-flight ingest tiles (F4) | a few hundred per flight | same as above | bounded by AC-8.3 carve-out | per flight |

### Data Management

**Seed data**: F1 pre-flight provisioning is the seeding step (operator runs C12 → C11 `TileDownloader` (tiles) → C10 (engines + descriptors + manifest) → C6).

**Rollback**: F1 is idempotent (D-C10-1 manifest-hash check). Mid-flight writes during F4 are append-only; on F8 reboot, partially-written tiles are detected via the SHA-256 content-hash gate (D-C10-3) at takeoff, and the corrupted tile is dropped.

## 5. Implementation Details

**Algorithmic Complexity**:
- HNSW search: `O(log N)` in corpus size.
- bbox query: btree index `O(log N)` per probe; result-set scan `O(R)` in matched rows.

**State Management**:
- `TileStore` is stateless (filesystem-backed).
- `TileMetadataStore` holds a Postgres connection pool.
- `DescriptorIndex` holds the FAISS reader + mmap'd index file handle; lifetime = flight.

**Key Dependencies**:

| Library | Version | Purpose |
|---------|---------|---------|
| PostgreSQL (server + libpq) | 16.x (mirror of `satellite-provider`'s pin) | Spatial metadata index |
| psycopg / asyncpg | per project pin | Python Postgres client |
| faiss-cpu (PyPI wheel) | `>=1.7,<2.0` | HNSW retrieval (AZ-306 chose the upstream wheel over a custom pybind11 wrapper; runtime-gated by `BUILD_FAISS_INDEX` at the storage factory) |
| atomicwrites | latest | Atomic file replacement for `.index` rebuild (D-C10-3) |
| hashlib (stdlib) | stdlib | SHA-256 content-hash sidecars |

**Error Handling Strategy**:
- `TileNotFoundError`: tile pixel file missing despite metadata row present. Log + drop the candidate; signal cache inconsistency to C13 for FDR.
- `TileFsError`: I/O error on filesystem read/write. F4 writes retry once with backoff; reads do not retry (the candidate is dropped).
- `TileMetadataError`: Postgres failure. Pre-flight: F1 fails fast; takeoff blocked. Mid-flight: F4 writes drop the tile (logged); reads for C2's top-K fail to enrich and downstream uses descriptor distances only (degraded but not fatal).
- `IndexUnavailableError`: FAISS handle invalid (e.g., file replaced concurrently). Treated as fatal in flight; F8 reboot recovery re-mmaps.

## 6. Extensions and Helpers

| Helper | Purpose | Used By |
|--------|---------|---------|
| `WgsConverter` | shared with C4, C5, C8 | C4, C5, C6 (bbox queries), C8 |
| `Sha256Sidecar` | atomic write + SHA-256 content-hash sidecar pattern (D-C10-3) | C6, C10 |
| `OrthorectifierUtils` | minimal orthorectification math (used by F4 write path) | C5, C6 — keep inside the F4 boundary; if needed by more components, promote |

## 7. Caveats & Edge Cases

**Known limitations**:
- The on-disk layout MUST mirror `satellite-provider` exactly so F10 upload is byte-identical. Any deviation breaks AC-8.4. Verified by NFT-SEC-01 and IT-7.
- Postgres on a Jetson is heavyweight; runtime resource budget validated by NFT-LIM-01 (8 h replay).

**Potential race conditions**:
- F4 writes can race with C2/C2.5/C3 reads on the same tile filesystem. Solution: F4 writes use `atomicwrites` for atomic rename; readers see either the old version or the new version, never partial.
- FAISS `.index` mmap is read-only during a flight; F1 would never overwrite during a flight (F1 happens pre-flight only).

**Performance bottlenecks**:
- Postgres metadata writes during F4 are bursty; if bursts approach 10 Hz, the Postgres connection becomes a bottleneck. Bound by configurable backpressure on the F4 path.
- FAISS HNSW first query pays the page-in cost (multi-second). F2 takeoff load forces a warm-up query so F3 first frame is cheap.

## 8. Dependency Graph

**Must be implemented after**: external `satellite-provider` on-disk layout reference (read its README + table schema; mirror exactly).

**Can be implemented in parallel with**: C7 — independent paths.

**Blocks**: C2, C2.5, C3, C10, C11 (both `TileDownloader` and `TileUploader`), F1, F2, F3, F4, F8, F10.

## 9. Logging Strategy

| Log Level | When | Example |
|-----------|------|---------|
| ERROR | `TileNotFoundError` (cache inconsistency); `IndexUnavailableError` | `Tile pixel missing for tile_id=(z=18,lat=49.71,lon=37.45)` |
| WARN | F4 write retried; bbox query slower than threshold | `F4 tile write retry: tile_id=…; reason=fs_busy` |
| INFO | Provisioning complete; index loaded | `C6 ready: tiles=87654, faiss_dim=512, index_size_mb=412` |
| DEBUG | per-query timing | `C6 search_topk frame=12345 took=0.4ms; bbox_query took=2.1ms` |

**Log format**: structured JSON.
**Log storage**: stdout / journald / FDR via C13 (ERROR + WARN only; the FDR also captures every successful F4 write as part of the mid-flight-tile-gen log).
