50ed780844
Foundation for the /photos/search/unified endpoint (Phase 2). Two new, fully unit-tested pieces, not yet wired into a route (allow-until-wired, mirroring llm_client.rs): - ai/nl_query.rs: translate a free-text query into a StructuredQuery via one grounded LLM call. Two-stage — the model emits names/ISO dates, then a pure resolve step maps tag names against the real vocab and converts dates to unix seconds. Hallucinated (non-vocab) tags are surfaced in unmatched_tags rather than silently used as hard filters — the anti-noise guard. 12 tests. - geo::forward_geocode + bbox_to_circle: resolve a place name to a circle via Nominatim /search, collapsing the bounding box to centroid + circumscribing radius so "Portland" and "Italy" both map onto the existing gps circle filter with no schema change. Radius is the max centroid-to-corner distance (corners aren't equidistant on a sphere). 4 tests. fmt + clippy clean; 19 new tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>