// // PlaceProvider.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // Turns the device location into a coarse PlaceRef (locality, never raw // coordinates — CLAUDE.md §4). Everything stays on-device; CLGeocoder // lookups are throttled and cached. // import CoreLocation import Foundation import Observation @MainActor @Observable final class PlaceProvider: NSObject, CLLocationManagerDelegate { private(set) var currentPlace: PlaceRef? private let manager = CLLocationManager() private let geocoder = CLGeocoder() private var lastGeocodedLocation: CLLocation? override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyHundredMeters } func startIfAuthorized() { switch manager.authorizationStatus { case .notDetermined: manager.requestWhenInUseAuthorization() case .authorizedWhenInUse, .authorizedAlways: manager.startUpdatingLocation() default: break } } func stop() { manager.stopUpdatingLocation() } nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { Task { @MainActor in self.startIfAuthorized() } } nonisolated func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { guard let location = locations.last else { return } Task { @MainActor in self.geocodeIfNeeded(location) } } private func geocodeIfNeeded(_ location: CLLocation) { // Re-geocode only after moving ~500 m; place names don't change faster. if let previous = lastGeocodedLocation, location.distance(from: previous) < 500 { return } lastGeocodedLocation = location Task { guard let placemark = try? await geocoder.reverseGeocodeLocation(location).first else { return } currentPlace = PlaceRef( name: placemark.areasOfInterest?.first, locality: placemark.locality, countryCode: placemark.isoCountryCode ) } } }