Spaces:
Runtime error
Runtime error
| """Load airline_routes.json and build in-memory route graph + search index.""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from dataclasses import dataclass, field | |
| class Route: | |
| destination: str | |
| distance_km: int | |
| duration_min: int | |
| carriers: list[dict] # [{"iata": "AA", "name": "American Airlines"}, ...] | |
| class Airport: | |
| iata: str | |
| name: str | |
| city_name: str | |
| country: str | |
| country_code: str | |
| continent: str | |
| latitude: float | |
| longitude: float | |
| timezone: str | |
| elevation: int | |
| icao: str | |
| display_name: str | |
| routes: list[Route] = field(default_factory=list) | |
| hub_score: float = 0.0 | |
| class RouteGraph: | |
| """In-memory route graph and search index.""" | |
| def __init__(self) -> None: | |
| self.airports: dict[str, Airport] = {} | |
| # route_map[origin_iata][dest_iata] = Route | |
| self.route_map: dict[str, dict[str, Route]] = {} | |
| # Search index: lowercase tokens → set of IATA codes | |
| self._search_index: dict[str, set[str]] = {} | |
| def load(self, filepath: str) -> None: | |
| with open(filepath) as f: | |
| data: dict = json.load(f) | |
| for iata, info in data.items(): | |
| routes = [] | |
| for r in info.get("routes", []): | |
| if not r["carriers"]: | |
| continue # Skip routes with no carrier data | |
| routes.append(Route( | |
| destination=r["iata"], | |
| distance_km=r["km"], | |
| duration_min=r["min"], | |
| carriers=r["carriers"], | |
| )) | |
| airport = Airport( | |
| iata=iata, | |
| name=info["name"], | |
| city_name=info["city_name"], | |
| country=info["country"], | |
| country_code=info["country_code"], | |
| continent=info["continent"], | |
| latitude=float(info["latitude"]) if info.get("latitude") is not None else 0.0, | |
| longitude=float(info["longitude"]) if info.get("longitude") is not None else 0.0, | |
| timezone=info.get("timezone", "UTC"), | |
| elevation=info.get("elevation", 0), | |
| icao=info.get("icao", ""), | |
| display_name=info.get("display_name", f"{info['city_name']} ({iata})"), | |
| routes=routes, | |
| ) | |
| self.airports[iata] = airport | |
| # Build route map | |
| self.route_map.setdefault(iata, {}) | |
| for route in routes: | |
| self.route_map[iata][route.destination] = route | |
| # Build search index | |
| self._index_airport(airport) | |
| def _index_airport(self, airport: Airport) -> None: | |
| tokens = set() | |
| # IATA code | |
| tokens.add(airport.iata.lower()) | |
| # City name tokens | |
| for word in airport.city_name.lower().split(): | |
| tokens.add(word) | |
| # Airport name tokens | |
| for word in airport.name.lower().split(): | |
| tokens.add(word) | |
| # Country | |
| for word in airport.country.lower().split(): | |
| tokens.add(word) | |
| # Country code | |
| tokens.add(airport.country_code.lower()) | |
| for token in tokens: | |
| # Index exact token and all prefixes ≥ 2 chars | |
| for i in range(2, len(token) + 1): | |
| prefix = token[:i] | |
| self._search_index.setdefault(prefix, set()).add(airport.iata) | |
| def search_airports(self, query: str, limit: int = 10) -> list[Airport]: | |
| """Search airports by IATA code, city, name, or country.""" | |
| q = query.strip().lower() | |
| if not q: | |
| return [] | |
| # Exact IATA match first | |
| if len(q) == 3 and q.upper() in self.airports: | |
| exact = self.airports[q.upper()] | |
| results = [exact] | |
| # Add more results from prefix search | |
| candidates = self._search_index.get(q, set()) | |
| for iata in candidates: | |
| if iata != exact.iata: | |
| results.append(self.airports[iata]) | |
| if len(results) >= limit: | |
| break | |
| return results[:limit] | |
| # Split query into tokens, intersect matches | |
| query_tokens = q.split() | |
| if not query_tokens: | |
| return [] | |
| # Get candidates matching first token | |
| candidates = self._search_index.get(query_tokens[0], set()).copy() | |
| # Intersect with additional tokens | |
| for token in query_tokens[1:]: | |
| token_matches = self._search_index.get(token, set()) | |
| candidates &= token_matches | |
| if not candidates: | |
| return [] | |
| # Sort by hub score (descending), then alphabetically | |
| airports = [self.airports[iata] for iata in candidates if iata in self.airports] | |
| airports.sort(key=lambda a: (-a.hub_score, a.city_name)) | |
| return airports[:limit] | |
| def get_direct_route(self, origin: str, destination: str) -> Route | None: | |
| return self.route_map.get(origin, {}).get(destination) | |
| def get_outbound_routes(self, origin: str) -> dict[str, Route]: | |
| return self.route_map.get(origin, {}) | |
| # Singleton | |
| _graph: RouteGraph | None = None | |
| def get_route_graph() -> RouteGraph: | |
| global _graph | |
| if _graph is None: | |
| _graph = RouteGraph() | |
| data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "airline_routes.json") | |
| _graph.load(data_path) | |
| return _graph | |