## Context

Phase 2's alert throttle (250 m / 10-min window) blocks repeat alerts from the same spot but doesn't distinguish *finders* — multiple people at the same spot only get one alert, and one device can re-alert by moving around. This change adds (A) a different-finder re-alert rule and (B) a 24 h per-device block via browser fingerprinting. Anti-spam motivation mirrors the 2014 system (which tracked fingerprint + IP on every alert).

## Goals / Non-Goals

**Goals:**
- Every distinct finder who leaves a phone number reaches the owner (within the window).
- One device cannot spam the owner more than once per 24 h.
- Minimal dependency footprint: hand-rolled fingerprint hash, no third-party JS.

**Non-Goals:**
- Identity-grade fingerprinting (this is anti-spam, not authentication; a determined attacker can fake signals).
- IP-based blocking (privacy + NAT false positives) — fingerprint only.
- Rate limits tied to plans/billing (Phase 5).

## Decisions

1. **Fingerprint = lightweight hand-rolled hash** of `navigator.userAgent`, `language`, `timezoneOffset`, `screen WxH`, `platform`, `hardwareConcurrency`. JS computes a 32-bit hash → base36 string. *Alternative:* fingerprintjs-lite (heavier, more robust) — deferred; the hand-rolled version is dependency-free and adequate for anti-spam; upgradeable later.

2. **Fingerprint block is checked FIRST** in `shouldAlert` (before the 10-min throttle): a fingerprint seen in the last 24 h ⇒ record-only. *Rationale:* the same device is the same person regardless of which location/phone they present.

3. **Different-finder rule lives inside the 10-min window branch**: within the window, re-alert if `moved > 250 m` OR (`newPhone != ""` AND `newPhone != lastAlert.scanner_phone`). Outside the window, alert as before.

4. **Contact path dedup**: `FinderContact` queries the last alerted scan; within the window and same phone ⇒ store-only (no SMS). Different phone ⇒ SMS. Fingerprint present and seen within 24 h ⇒ store-only.

5. **Schema**: `ALTER TABLE scans ADD COLUMN IF NOT EXISTS fingerprint TEXT;` (idempotent, nullable). New sqlc query `GetRecentScanByFingerprint` (`WHERE fingerprint = $1 AND scanned_at > now() - interval '24 hours' ORDER BY scanned_at DESC LIMIT 1`).

6. **ScanRequest gains optional `phone` + `fingerprint`**; both flow into `InsertScan`.

## Risks / Trade-offs

- [Fingerprint is spoofable] → accepted (anti-spam, not security); document as such.
- [Different-phone rule could alert twice from one person with two numbers] → acceptable; the owner sees both contacts and the 24 h fingerprint block caps same-device repeats.
- [Fingerprint column grows] → TEXT, nullable, negligible.

## Migration Plan

1. `make db-up` (idempotent ALTER) → `scans.fingerprint`.
2. sqlc `make generate` (new query + column in model).
3. Deploy code + JS; no data migration needed.

## Open Questions

- None blocking. (24 h window is a constant; adjustable later.)
