// // SearchFilter.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // A natural query becomes this structured filter, which runs on the // local database — instant on 40 000 photos as on 400 (CLAUDE.md §7). // The model analyzes the query; it never walks the photos. // import Foundation struct SearchFilter: Codable, Sendable, Equatable { var dateRange: ClosedRange? var kinds: [String] // PhotoKind raw values var textTerms: [String] // matched against OCR text var entityTerms: [String] // matched against entities / labels / hints var placeTerms: [String] var projectTerm: String? var similarToIdentifier: String? // "photos like this one" var favoritesOnly: Bool var actionableOnly: Bool // "documents avec une date d'expiration" static let empty = SearchFilter( dateRange: nil, kinds: [], textTerms: [], entityTerms: [], placeTerms: [], projectTerm: nil, similarToIdentifier: nil, favoritesOnly: false, actionableOnly: false ) var isEmpty: Bool { self == .empty } } extension String { /// Case- and diacritic-insensitive folding for French matching /// ("reçu" == "recu", "Éléonore" == "eleonore"). var searchFolded: String { folding(options: [.caseInsensitive, .diacriticInsensitive], locale: Locale(identifier: "fr_CA")) } }