| enum AddressHelper {
// MARK: - Borough detection
static let boroughOrder = ["Manhattan", "Brooklyn", "Queens", "Bronx", "Staten Island"]
/// Derives the conventional postal borough/city name from an address string.
static func borough(for address: String) -> String {
// Strip trailing ", United States" / ", USA" that Apple Maps sometimes appends.
var raw = address
for suffix in [", United States", ", USA"] where raw.hasSuffix(suffix) {
raw = String(raw.dropLast(suffix.count)).trimmingCharacters(in: .whitespaces)
break
}
let pattern = #",\s*([^,]+),\s*[A-Za-z]{2,}(?:\s+\d{5}(?:-\d{4})?)?\s*$"#
guard let regex = try? NSRegularExpression(pattern: pattern),
let match = regex.firstMatch(in: raw,
range: NSRange(raw.startIndex..., in: raw)),
let range = Range(match.range(at: 1), in: raw) else { return "Other" }
let city = String(raw[range]).trimmingCharacters(in: .whitespaces)
switch city.lowercased() {
case "new york", "new york city", "manhattan": return "Manhattan"
case "brooklyn": return "Brooklyn"
case "queens", "astoria", "long island city",
"flushing", "jamaica", "forest hills": return "Queens"
case "bronx", "the bronx": return "Bronx"
case "staten island": return "Staten Island"
default: return city.isEmpty ? "Other" : city
}
}
// MARK: - Address migration
/// Cleans up an address string the same way EntryStore.load() does on disk.
/// Safe to call repeatedly — idempotent.
static func migratedAddress(_ address: String) -> String {
var addr = address
// 1. Strip trailing ", United States" / ", USA"
for suffix in [", United States", ", USA"] where addr.hasSuffix(suffix) {
addr = String(addr.dropLast(suffix.count)).trimmingCharacters(in: .whitespaces)
break
}
// 2. "Manhattan, NY" → "New York, NY"
addr = addr.replacingOccurrences(
of: "Manhattan, NY", with: "New York, NY", options: .caseInsensitive)
return addr
}
// MARK: - Search matching
/// Returns true when the entry matches the given (possibly empty) search query.
static func matches(entry: SpotEntry, query: String) -> Bool {
guard !query.isEmpty else { return true }
let q = query.lowercased()
return entry.playName.lowercased().contains(q)
|| entry.cuisine.lowercased().contains(q)
|| entry.neighborhood.lowercased().contains(q)
|| entry.notes.lowercased().contains(q)
}
// MARK: - Entry sorting (pure, no location dependency)
/// Sorts entries by the given order. Distance sort falls back to alphabetical
/// (the caller must supply pre-sorted entries when a live location is available).
static func sort(_ entries: [SpotEntry], by order: SortOrder) -> [SpotEntry] {
switch order {
case .default:
return entries.sorted { $0.playName.lowercased() < $1.playName.lowercased() }
case .cuisine:
return entries.sorted { $0.cuisine.lowercased() < $1.cuisine.lowercased() }
case .neighborhood:
return entries.sorted { $0.neighborhood.lowercased() < $1.neighborhood.lowercased() }
case .distance:
return entries.sorted { $0.playName.lowercased() < $1.playName.lowercased() }
case .ratings:
return entries.sorted { $0.starRating > $1.starRating }
}
}
} | `AddressHelper` enum | Defines the `AddressHelper` enum. |