v1.4.0
What's Changed
Bug Fixes & Hardening (code-review findings 5–8)
Finding 5 — Reliable payload size enforcement (BundledJSONDataSource)
Removed the unreliable url.resourceValues(forKeys: [.fileSizeKey]) pre-flight size check, which silently no-ops on sandboxed or virtual filesystems. The post-read data.count guard is now the sole, authoritative enforcement point.
Finding 6 — Query clamp at the correct altitude (AddressSearchEngine, KhmerNormalizer)
Moved the maxQueryLength truncation from the shared KhmerNormalizer utility into AddressSearchEngine.search() — the correct layer (user-query entry point). The normalizer is now a pure text utility with no search-specific concerns; the clamp no longer fires during index building.
Finding 7 — Consistent traversal contract (AddressStore)
districts(inProvince:), communes(inDistrict:), and villages(inCommune:) now throw AddressError.notFound for unknown parent codes instead of silently returning []. Callers can now reliably distinguish "valid empty list" from "unrecognised code" — consistent with selection(forVillageCode:) which has always thrown.
Finding 8 — Shared cache decode path + async file I/O (DatasetCache, DatasetDecoding, CachingDataSource)
- Added
DatasetDecoding.decodeDataset()for the domain-model cache schema (full property names, distinct from the wire format's short keys).DatasetCache.read()now routes through it for consistentAddressError.decodingFailedmapping. - Added
DatasetCache.readAsync()which offloads blocking file I/O toDispatchQueue.global(qos: .userInitiated).CachingDataSource.bestAvailable(),version, andrefresh()all use this path, keeping the Swift cooperative thread pool unblocked.
Tests
unknownParentReturnsEmpty→unknownParentThrowsNotFound(reflects new throwing contract)- Clamp coverage moved from
KhmerNormalizerTeststoAddressSearchEngineTests.clampsOverlongQuery - 138 tests passing, zero warnings under Swift 6 strict concurrency