feat: improve planning engine travel handling, itinerary reordering, and scenario planners

Add TravelInfo initializers and city normalization helpers to fix repeat
city-pair disambiguation. Improve drag-and-drop reordering with segment
index tracking and source-row-aware zone calculation. Enhance all five
scenario planners with better next-day departure handling and travel
segment placement. Add comprehensive tests across all planners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-13 08:55:23 -06:00
parent 1c97f35754
commit 9736773475
19 changed files with 928 additions and 171 deletions

View File

@@ -295,12 +295,42 @@ final class ScenarioCPlanner: ScenarioPlanner {
cityName: String,
stadiums: [String: Stadium]
) -> [Stadium] {
let normalizedCity = cityName.lowercased().trimmingCharacters(in: .whitespaces)
let normalizedCity = normalizeCityName(cityName)
return stadiums.values.filter { stadium in
stadium.city.lowercased().trimmingCharacters(in: .whitespaces) == normalizedCity
let normalizedStadiumCity = normalizeCityName(stadium.city)
if normalizedStadiumCity == normalizedCity { return true }
return normalizedStadiumCity.contains(normalizedCity) || normalizedCity.contains(normalizedStadiumCity)
}
}
/// Normalizes city labels for resilient user-input matching.
private func normalizeCityName(_ raw: String) -> String {
// Keep the city component before state suffixes like "City, ST".
let cityPart = raw.split(separator: ",", maxSplits: 1).first.map(String.init) ?? raw
var normalized = cityPart
.lowercased()
.replacingOccurrences(of: ".", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
let aliases: [String: String] = [
"nyc": "new york",
"new york city": "new york",
"la": "los angeles",
"sf": "san francisco",
"dc": "washington",
"washington dc": "washington"
]
if let aliased = aliases[normalized] {
normalized = aliased
}
// Collapse repeated spaces after punctuation/alias normalization.
return normalized
.split(whereSeparator: \.isWhitespace)
.joined(separator: " ")
}
/// Finds stadiums that make forward progress from start to end.
///
/// A stadium is "directional" if visiting it doesn't significantly increase