# H3 Indexing + Classify

**Task**: AZ-665_mapobjects_store_h3_classify
**Name**: H3 indexing + k-ring classify(detection) → new/moved/existing
**Description**: Compute H3 cell for each detection at the configured resolution (default 10, ~15 m edge). Maintain in-memory `(H3_cell + class) → MapObject` hashmap. Answer `classify(detection)` using k-ring (k=2 default) lookup against `(distance_threshold_m, move_threshold_m, similar_classes)` config.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure
**Component**: mapobjects_store
**Tracker**: AZ-665
**Epic**: AZ-633

## Problem

The H3 spatial index is the foundation of new-vs-existing detection (`architecture.md §7.12`). Each detection's MGRS position is converted to an H3 cell at the configured resolution; the composite key `(H3_cell, class)` keys an in-memory map of known MapObjects. Classification answers `new | moved | existing` by querying the k-ring of cells (boundary correctness) and computing distance against move thresholds.

## Outcome

- `H3Index::cell_of(mgrs, resolution) -> H3Cell`.
- `MapObjectsStore::classify(detection) -> MapObjectClassification ∈ {New, Moved { from_mgrs, to_mgrs }, Existing { existing_id }}`.
- k-ring lookup (default k=2) over the in-memory hashmap.
- `distance_threshold_m` (default 30 m), `move_threshold_m` (default 50 m), `similar_classes` (configured set per `data_model.md §IgnoredItem` class groups) read from config.
- O(1) classify p99 ≤1 ms.

## Scope

### Included
- H3 binding (Rust crate `h3o` or equivalent).
- `MapObjectsStore` struct + in-memory hashmap.
- `classify` API.
- Config-driven thresholds.

### Excluded
- IgnoredItem suppression (task 27).
- Pre-flight hydrate + sync_state machine (task 28).
- Persistence (task 29).
- End-of-pass removed-candidate sweep (task 27).

## Acceptance Criteria

**AC-1: New detection at unseen MGRS**
Given an empty store
When `classify(detection_at_M1, class=A)` is called
Then it returns `Classification::New`.

**AC-2: Existing detection at known MGRS within threshold**
Given the store has a MapObject at `M1, class=A`
When `classify(detection_at_M1+5m, class=A)` is called and `distance_threshold_m = 30`
Then it returns `Classification::Existing { existing_id: ... }`.

**AC-3: Moved detection beyond move threshold**
Given the store has a MapObject at `M1, class=A`
When `classify(detection_at_M1+60m, class=A)` is called and `move_threshold_m = 50`
Then it returns `Classification::Moved { from_mgrs: M1, to_mgrs: M1+60m }`.

**AC-4: k-ring boundary lookup**
Given the store has a MapObject in cell `C1`
When a new detection falls in cell `C2` (boundary cell of `C1`)
Then with k=2 the lookup finds `C1` and returns `Existing` (not `New`).

**AC-5: Classify p99 ≤1 ms**
Given a store warmed with 10 000 MapObjects
When `classify` is called 1 000 times
Then p99 latency is ≤1 ms.

## Non-Functional Requirements

**Performance**
- O(1) classify p99 ≤1 ms (per `description.md §9`).

**Reliability**
- k-ring boundary correctness guaranteed by default config.

## Contract

- Canonical typed model: `data_model.md §MapObject`, `§MapObjectClassification`.

## Runtime Completeness

- **Named capability**: H3 spatial index + k-ring queries — production new/moved/existing dispatch.
- **Production code that must exist**: real H3 crate; real k-ring lookup.
- **Unacceptable substitutes**: Euclidean-distance-only naive search is unacceptable for production (loses boundary correctness and O(1) latency).
