Skip to main content

airport_data/
lib.rs

1//! # Airport Data
2//!
3//! A comprehensive Rust library for retrieving airport information by IATA codes,
4//! ICAO codes, and various other criteria. Provides access to a large dataset of
5//! airports worldwide with detailed information including coordinates, timezone,
6//! type, and external links.
7//!
8//! ## Quick Start
9//!
10//! ```rust
11//! use airport_data::AirportData;
12//!
13//! let db = AirportData::new();
14//!
15//! // Get airport by IATA code
16//! let airport = db.get_airport_by_iata("SIN").unwrap();
17//! assert_eq!(airport.airport, "Singapore Changi Airport");
18//!
19//! // Get airport by ICAO code
20//! let airport = db.get_airport_by_icao("WSSS").unwrap();
21//! assert_eq!(airport.country_code, "SG");
22//! ```
23
24use flate2::read::GzDecoder;
25use once_cell::sync::Lazy;
26use serde::{Deserialize, Deserializer, Serialize};
27use std::collections::HashMap;
28use std::f64::consts::PI;
29use std::io::Read;
30
31// Embed the compressed data at compile time
32static COMPRESSED_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/airports.json.gz"));
33
34static AIRPORTS: Lazy<Vec<Airport>> = Lazy::new(|| {
35    let mut decoder = GzDecoder::new(COMPRESSED_DATA);
36    let mut json_str = String::new();
37    decoder
38        .read_to_string(&mut json_str)
39        .expect("Failed to decompress airport data");
40    serde_json::from_str(&json_str).expect("Failed to parse airport data")
41});
42
43// ---------------------------------------------------------------------------
44// Custom deserializer helpers
45// ---------------------------------------------------------------------------
46
47/// Deserializes a value that can be an integer, float, or an empty string into Option<i64>.
48fn deserialize_opt_i64<'de, D>(deserializer: D) -> std::result::Result<Option<i64>, D::Error>
49where
50    D: Deserializer<'de>,
51{
52    #[derive(Deserialize)]
53    #[serde(untagged)]
54    enum NumOrStr {
55        Int(i64),
56        Float(f64),
57        Str(String),
58    }
59    match NumOrStr::deserialize(deserializer)? {
60        NumOrStr::Int(n) => Ok(Some(n)),
61        NumOrStr::Float(f) => Ok(Some(f as i64)),
62        NumOrStr::Str(s) if s.is_empty() => Ok(None),
63        NumOrStr::Str(s) => s.parse::<i64>().ok().map_or(Ok(None), |n| Ok(Some(n))),
64    }
65}
66
67/// Deserializes a value that can be an integer, float, or an empty string into Option<f64>.
68fn deserialize_opt_f64<'de, D>(deserializer: D) -> std::result::Result<Option<f64>, D::Error>
69where
70    D: Deserializer<'de>,
71{
72    #[derive(Deserialize)]
73    #[serde(untagged)]
74    enum NumOrStr {
75        Int(i64),
76        Float(f64),
77        Str(String),
78    }
79    match NumOrStr::deserialize(deserializer)? {
80        NumOrStr::Int(n) => Ok(Some(n as f64)),
81        NumOrStr::Float(f) => Ok(Some(f)),
82        NumOrStr::Str(s) if s.is_empty() => Ok(None),
83        NumOrStr::Str(s) => s.parse::<f64>().ok().map_or(Ok(None), |n| Ok(Some(n))),
84    }
85}
86
87/// Deserializes a value that can be a string or an integer into a String.
88fn deserialize_string_or_int<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
89where
90    D: Deserializer<'de>,
91{
92    #[derive(Deserialize)]
93    #[serde(untagged)]
94    enum StringOrInt {
95        Str(String),
96        Int(i64),
97    }
98    match StringOrInt::deserialize(deserializer)? {
99        StringOrInt::Str(s) => Ok(s),
100        StringOrInt::Int(n) => Ok(n.to_string()),
101    }
102}
103
104/// Deserializes "TRUE"/"FALSE" strings into bool.
105fn deserialize_bool_from_string<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
106where
107    D: Deserializer<'de>,
108{
109    let s = String::deserialize(deserializer)?;
110    Ok(s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("yes"))
111}
112
113// ---------------------------------------------------------------------------
114// Airport struct
115// ---------------------------------------------------------------------------
116
117/// Represents a single airport with all its associated data.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct Airport {
120    /// 3-letter IATA code (may be empty for some airports)
121    pub iata: String,
122    /// 4-letter ICAO code (may be empty for some airports)
123    #[serde(deserialize_with = "deserialize_string_or_int")]
124    pub icao: String,
125    /// Timezone identifier (e.g. "Asia/Singapore")
126    #[serde(rename = "time")]
127    pub timezone: String,
128    /// UTC offset (can be fractional, e.g. 5.5 for India; None if unavailable)
129    #[serde(deserialize_with = "deserialize_opt_f64")]
130    pub utc: Option<f64>,
131    /// 2-letter country code
132    pub country_code: String,
133    /// 2-letter continent code (AS, EU, NA, SA, AF, OC, AN)
134    pub continent: String,
135    /// Airport name
136    pub airport: String,
137    /// Latitude coordinate
138    pub latitude: f64,
139    /// Longitude coordinate
140    pub longitude: f64,
141    /// Elevation in feet (may be None if data unavailable)
142    #[serde(deserialize_with = "deserialize_opt_i64")]
143    pub elevation_ft: Option<i64>,
144    /// Airport type (large_airport, medium_airport, small_airport, heliport, seaplane_base)
145    #[serde(rename = "type")]
146    pub airport_type: String,
147    /// Whether the airport has scheduled commercial service
148    #[serde(deserialize_with = "deserialize_bool_from_string")]
149    pub scheduled_service: bool,
150    /// Wikipedia URL
151    #[serde(default)]
152    pub wikipedia: String,
153    /// Airport website URL
154    #[serde(default)]
155    pub website: String,
156    /// Longest runway length in feet (may be None if data unavailable)
157    #[serde(deserialize_with = "deserialize_opt_i64")]
158    pub runway_length: Option<i64>,
159    /// FlightRadar24 URL
160    #[serde(default)]
161    pub flightradar24_url: String,
162    /// RadarBox URL
163    #[serde(default)]
164    pub radarbox_url: String,
165    /// FlightAware URL
166    #[serde(default)]
167    pub flightaware_url: String,
168}
169
170// ---------------------------------------------------------------------------
171// Result types
172// ---------------------------------------------------------------------------
173
174/// External links for an airport.
175#[derive(Debug, Clone, Serialize)]
176pub struct AirportLinks {
177    pub website: Option<String>,
178    pub wikipedia: Option<String>,
179    pub flightradar24: Option<String>,
180    pub radarbox: Option<String>,
181    pub flightaware: Option<String>,
182}
183
184/// Statistics for airports in a country.
185#[derive(Debug, Clone, Serialize)]
186pub struct CountryStats {
187    pub total: usize,
188    pub by_type: HashMap<String, usize>,
189    pub with_scheduled_service: usize,
190    pub average_runway_length: f64,
191    pub average_elevation: f64,
192    pub timezones: Vec<String>,
193}
194
195/// Statistics for airports on a continent.
196#[derive(Debug, Clone, Serialize)]
197pub struct ContinentStats {
198    pub total: usize,
199    pub by_type: HashMap<String, usize>,
200    pub by_country: HashMap<String, usize>,
201    pub with_scheduled_service: usize,
202    pub average_runway_length: f64,
203    pub average_elevation: f64,
204    pub timezones: Vec<String>,
205}
206
207/// Information about an airport in a distance matrix.
208#[derive(Debug, Clone, Serialize)]
209pub struct AirportInfo {
210    pub code: String,
211    pub name: String,
212    pub iata: String,
213    pub icao: String,
214}
215
216/// Distance matrix result.
217#[derive(Debug, Clone, Serialize)]
218pub struct DistanceMatrix {
219    pub airports: Vec<AirportInfo>,
220    pub distances: HashMap<String, HashMap<String, f64>>,
221}
222
223/// An airport with its distance from a reference point.
224#[derive(Debug, Clone)]
225pub struct NearbyAirport {
226    pub airport: Airport,
227    /// Distance in kilometers.
228    pub distance: f64,
229}
230
231/// Filters for advanced airport search.
232#[derive(Debug, Clone, Default)]
233pub struct AirportFilter {
234    pub country_code: Option<String>,
235    pub continent: Option<String>,
236    pub airport_type: Option<String>,
237    pub has_scheduled_service: Option<bool>,
238    pub min_runway_ft: Option<i64>,
239}
240
241/// Error types for the library.
242#[derive(Debug, Clone)]
243pub enum AirportError {
244    /// No data found for the given code/query.
245    NotFound(String),
246    /// Invalid input format.
247    InvalidInput(String),
248}
249
250impl std::fmt::Display for AirportError {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        match self {
253            AirportError::NotFound(msg) => write!(f, "{}", msg),
254            AirportError::InvalidInput(msg) => write!(f, "{}", msg),
255        }
256    }
257}
258
259impl std::error::Error for AirportError {}
260
261pub type Result<T> = std::result::Result<T, AirportError>;
262
263// ---------------------------------------------------------------------------
264// Haversine distance
265// ---------------------------------------------------------------------------
266
267fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
268    const EARTH_RADIUS_KM: f64 = 6371.0;
269    let d_lat = (lat2 - lat1) * PI / 180.0;
270    let d_lon = (lon2 - lon1) * PI / 180.0;
271    let lat1_rad = lat1 * PI / 180.0;
272    let lat2_rad = lat2 * PI / 180.0;
273
274    let a =
275        (d_lat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
276    let c = 2.0 * a.sqrt().asin();
277    EARTH_RADIUS_KM * c
278}
279
280// ---------------------------------------------------------------------------
281// Helper: match airport type filter
282// ---------------------------------------------------------------------------
283
284fn type_matches(airport_type: &str, filter_type: &str) -> bool {
285    let filter_lower = filter_type.to_lowercase();
286    if filter_lower == "airport" {
287        let at = airport_type.to_lowercase();
288        at == "large_airport" || at == "medium_airport" || at == "small_airport"
289    } else {
290        airport_type.eq_ignore_ascii_case(&filter_lower)
291    }
292}
293
294/// Helper to apply AirportFilter to an airport.
295fn matches_filter(airport: &Airport, filter: &AirportFilter) -> bool {
296    if let Some(ref cc) = filter.country_code {
297        if !airport.country_code.eq_ignore_ascii_case(cc) {
298            return false;
299        }
300    }
301    if let Some(ref cont) = filter.continent {
302        if !airport.continent.eq_ignore_ascii_case(cont) {
303            return false;
304        }
305    }
306    if let Some(ref t) = filter.airport_type {
307        if !type_matches(&airport.airport_type, t) {
308            return false;
309        }
310    }
311    if let Some(has_service) = filter.has_scheduled_service {
312        if airport.scheduled_service != has_service {
313            return false;
314        }
315    }
316    if let Some(min_runway) = filter.min_runway_ft {
317        let runway = airport.runway_length.unwrap_or(0);
318        if runway < min_runway {
319            return false;
320        }
321    }
322    true
323}
324
325fn non_empty(s: &str) -> Option<String> {
326    if s.is_empty() {
327        None
328    } else {
329        Some(s.to_string())
330    }
331}
332
333// ---------------------------------------------------------------------------
334// AirportData - main public API
335// ---------------------------------------------------------------------------
336
337/// The main entry point for querying airport data.
338///
339/// All data is loaded lazily on first access and cached for the lifetime of
340/// the process. The `AirportData` struct itself is zero-cost to create — it
341/// just provides methods to query the shared global data.
342pub struct AirportData;
343
344impl AirportData {
345    /// Creates a new `AirportData` instance.
346    ///
347    /// This is cheap — data is loaded lazily on first query.
348    pub fn new() -> Self {
349        AirportData
350    }
351
352    /// Returns a reference to all airports in the database.
353    pub fn all_airports(&self) -> &[Airport] {
354        &AIRPORTS
355    }
356
357    // =======================================================================
358    // Core Search Functions
359    // =======================================================================
360
361    /// Finds an airport by its 3-letter IATA code.
362    ///
363    /// Returns the first matching airport, or an error if not found.
364    ///
365    /// # Example
366    /// ```
367    /// use airport_data::AirportData;
368    /// let db = AirportData::new();
369    /// let airport = db.get_airport_by_iata("LHR").unwrap();
370    /// assert!(airport.airport.contains("Heathrow"));
371    /// ```
372    pub fn get_airport_by_iata(&self, iata_code: &str) -> Result<&Airport> {
373        let code = iata_code.trim().to_uppercase();
374        AIRPORTS
375            .iter()
376            .find(|a| a.iata.eq_ignore_ascii_case(&code))
377            .ok_or_else(|| {
378                AirportError::NotFound(format!("No data found for IATA code: {}", iata_code))
379            })
380    }
381
382    /// Finds all airports matching a 3-letter IATA code.
383    ///
384    /// Returns a `Vec` of references (usually 0 or 1 results).
385    pub fn get_airports_by_iata(&self, iata_code: &str) -> Vec<&Airport> {
386        let code = iata_code.trim().to_uppercase();
387        AIRPORTS
388            .iter()
389            .filter(|a| a.iata.eq_ignore_ascii_case(&code))
390            .collect()
391    }
392
393    /// Finds an airport by its 4-letter ICAO code.
394    ///
395    /// Returns the first matching airport, or an error if not found.
396    pub fn get_airport_by_icao(&self, icao_code: &str) -> Result<&Airport> {
397        let code = icao_code.trim().to_uppercase();
398        AIRPORTS
399            .iter()
400            .find(|a| a.icao.eq_ignore_ascii_case(&code))
401            .ok_or_else(|| {
402                AirportError::NotFound(format!("No data found for ICAO code: {}", icao_code))
403            })
404    }
405
406    /// Finds all airports matching a 4-letter ICAO code.
407    pub fn get_airports_by_icao(&self, icao_code: &str) -> Vec<&Airport> {
408        let code = icao_code.trim().to_uppercase();
409        AIRPORTS
410            .iter()
411            .filter(|a| a.icao.eq_ignore_ascii_case(&code))
412            .collect()
413    }
414
415    /// Searches for airports by name (case-insensitive partial match).
416    ///
417    /// Query must be at least 2 characters.
418    pub fn search_by_name(&self, query: &str) -> Result<Vec<&Airport>> {
419        let q = query.trim();
420        if q.len() < 2 {
421            return Err(AirportError::InvalidInput(
422                "Search query must be at least 2 characters".to_string(),
423            ));
424        }
425        let lower = q.to_lowercase();
426        let results: Vec<&Airport> = AIRPORTS
427            .iter()
428            .filter(|a| a.airport.to_lowercase().contains(&lower))
429            .collect();
430        Ok(results)
431    }
432
433    // =======================================================================
434    // Geographic Functions
435    // =======================================================================
436
437    /// Finds airports within a specified radius (km) of given coordinates.
438    ///
439    /// Results are sorted by distance (nearest first).
440    pub fn find_nearby_airports(&self, lat: f64, lon: f64, radius_km: f64) -> Vec<NearbyAirport> {
441        let mut results: Vec<NearbyAirport> = AIRPORTS
442            .iter()
443            .filter_map(|a| {
444                let dist = haversine_distance(lat, lon, a.latitude, a.longitude);
445                if dist <= radius_km {
446                    Some(NearbyAirport {
447                        airport: a.clone(),
448                        distance: dist,
449                    })
450                } else {
451                    None
452                }
453            })
454            .collect();
455        results.sort_by(|a, b| {
456            a.distance
457                .partial_cmp(&b.distance)
458                .unwrap_or(std::cmp::Ordering::Equal)
459        });
460        results
461    }
462
463    /// Calculates the great-circle distance between two airports in kilometers.
464    ///
465    /// Accepts either IATA or ICAO codes.
466    pub fn calculate_distance(&self, code1: &str, code2: &str) -> Result<f64> {
467        let a1 = self.resolve_airport(code1)?;
468        let a2 = self.resolve_airport(code2)?;
469        Ok(haversine_distance(
470            a1.latitude,
471            a1.longitude,
472            a2.latitude,
473            a2.longitude,
474        ))
475    }
476
477    /// Finds the single nearest airport to given coordinates.
478    ///
479    /// Optionally applies filters to narrow the search.
480    pub fn find_nearest_airport(
481        &self,
482        lat: f64,
483        lon: f64,
484        filter: Option<&AirportFilter>,
485    ) -> Result<NearbyAirport> {
486        let mut best: Option<NearbyAirport> = None;
487
488        for airport in AIRPORTS.iter() {
489            if let Some(f) = filter {
490                if !matches_filter(airport, f) {
491                    continue;
492                }
493            }
494            let dist = haversine_distance(lat, lon, airport.latitude, airport.longitude);
495            let is_closer = match &best {
496                Some(b) => dist < b.distance,
497                None => true,
498            };
499            if is_closer {
500                best = Some(NearbyAirport {
501                    airport: airport.clone(),
502                    distance: dist,
503                });
504            }
505        }
506
507        best.ok_or_else(|| {
508            AirportError::NotFound("No airport found matching the criteria".to_string())
509        })
510    }
511
512    // =======================================================================
513    // Filtering Functions
514    // =======================================================================
515
516    /// Finds all airports in a specific country.
517    pub fn get_airports_by_country_code(&self, country_code: &str) -> Vec<&Airport> {
518        let cc = country_code.trim().to_uppercase();
519        AIRPORTS
520            .iter()
521            .filter(|a| a.country_code.eq_ignore_ascii_case(&cc))
522            .collect()
523    }
524
525    /// Finds all airports on a specific continent.
526    ///
527    /// Continent codes: AS, EU, NA, SA, AF, OC, AN
528    pub fn get_airports_by_continent(&self, continent_code: &str) -> Vec<&Airport> {
529        let cc = continent_code.trim().to_uppercase();
530        AIRPORTS
531            .iter()
532            .filter(|a| a.continent.eq_ignore_ascii_case(&cc))
533            .collect()
534    }
535
536    /// Finds airports by their type.
537    ///
538    /// Types: `large_airport`, `medium_airport`, `small_airport`, `heliport`,
539    /// `seaplane_base`. Use `"airport"` to match all airport types (large,
540    /// medium, small).
541    pub fn get_airports_by_type(&self, airport_type: &str) -> Vec<&Airport> {
542        AIRPORTS
543            .iter()
544            .filter(|a| type_matches(&a.airport_type, airport_type))
545            .collect()
546    }
547
548    /// Finds all airports within a specific timezone.
549    pub fn get_airports_by_timezone(&self, timezone: &str) -> Vec<&Airport> {
550        AIRPORTS.iter().filter(|a| a.timezone == timezone).collect()
551    }
552
553    /// Finds airports matching multiple criteria.
554    pub fn find_airports(&self, filter: &AirportFilter) -> Vec<&Airport> {
555        AIRPORTS
556            .iter()
557            .filter(|a| matches_filter(a, filter))
558            .collect()
559    }
560
561    // =======================================================================
562    // Autocomplete & Links
563    // =======================================================================
564
565    /// Provides autocomplete suggestions for search interfaces.
566    ///
567    /// Returns up to 10 airports matching the query by name or IATA code.
568    /// Query must be at least 2 characters.
569    pub fn get_autocomplete_suggestions(&self, query: &str) -> Vec<&Airport> {
570        let q = query.trim();
571        if q.len() < 2 {
572            return Vec::new();
573        }
574        let lower = q.to_lowercase();
575        AIRPORTS
576            .iter()
577            .filter(|a| {
578                a.airport.to_lowercase().contains(&lower) || a.iata.to_lowercase().contains(&lower)
579            })
580            .take(10)
581            .collect()
582    }
583
584    /// Gets external links for an airport.
585    ///
586    /// Accepts either IATA or ICAO code.
587    pub fn get_airport_links(&self, code: &str) -> Result<AirportLinks> {
588        let airport = self.resolve_airport(code)?;
589        Ok(AirportLinks {
590            website: non_empty(&airport.website),
591            wikipedia: non_empty(&airport.wikipedia),
592            flightradar24: non_empty(&airport.flightradar24_url),
593            radarbox: non_empty(&airport.radarbox_url),
594            flightaware: non_empty(&airport.flightaware_url),
595        })
596    }
597
598    // =======================================================================
599    // Statistical & Analytical Functions
600    // =======================================================================
601
602    /// Gets comprehensive statistics about airports in a specific country.
603    pub fn get_airport_stats_by_country(&self, country_code: &str) -> Result<CountryStats> {
604        let airports = self.get_airports_by_country_code(country_code);
605        if airports.is_empty() {
606            return Err(AirportError::NotFound(format!(
607                "No airports found for country code: {}",
608                country_code
609            )));
610        }
611
612        let mut by_type: HashMap<String, usize> = HashMap::new();
613        let mut with_scheduled_service = 0usize;
614        let mut runway_sum = 0f64;
615        let mut runway_count = 0usize;
616        let mut elevation_sum = 0f64;
617        let mut elevation_count = 0usize;
618        let mut tz_set: Vec<String> = Vec::new();
619
620        for a in &airports {
621            *by_type.entry(a.airport_type.clone()).or_insert(0) += 1;
622            if a.scheduled_service {
623                with_scheduled_service += 1;
624            }
625            if let Some(r) = a.runway_length {
626                if r > 0 {
627                    runway_sum += r as f64;
628                    runway_count += 1;
629                }
630            }
631            if let Some(e) = a.elevation_ft {
632                elevation_sum += e as f64;
633                elevation_count += 1;
634            }
635            if !tz_set.contains(&a.timezone) {
636                tz_set.push(a.timezone.clone());
637            }
638        }
639
640        tz_set.sort();
641
642        Ok(CountryStats {
643            total: airports.len(),
644            by_type,
645            with_scheduled_service,
646            average_runway_length: if runway_count > 0 {
647                (runway_sum / runway_count as f64).round()
648            } else {
649                0.0
650            },
651            average_elevation: if elevation_count > 0 {
652                (elevation_sum / elevation_count as f64).round()
653            } else {
654                0.0
655            },
656            timezones: tz_set,
657        })
658    }
659
660    /// Gets comprehensive statistics about airports on a specific continent.
661    pub fn get_airport_stats_by_continent(&self, continent_code: &str) -> Result<ContinentStats> {
662        let airports = self.get_airports_by_continent(continent_code);
663        if airports.is_empty() {
664            return Err(AirportError::NotFound(format!(
665                "No airports found for continent code: {}",
666                continent_code
667            )));
668        }
669
670        let mut by_type: HashMap<String, usize> = HashMap::new();
671        let mut by_country: HashMap<String, usize> = HashMap::new();
672        let mut with_scheduled_service = 0usize;
673        let mut runway_sum = 0f64;
674        let mut runway_count = 0usize;
675        let mut elevation_sum = 0f64;
676        let mut elevation_count = 0usize;
677        let mut tz_set: Vec<String> = Vec::new();
678
679        for a in &airports {
680            *by_type.entry(a.airport_type.clone()).or_insert(0) += 1;
681            *by_country.entry(a.country_code.clone()).or_insert(0) += 1;
682            if a.scheduled_service {
683                with_scheduled_service += 1;
684            }
685            if let Some(r) = a.runway_length {
686                if r > 0 {
687                    runway_sum += r as f64;
688                    runway_count += 1;
689                }
690            }
691            if let Some(e) = a.elevation_ft {
692                elevation_sum += e as f64;
693                elevation_count += 1;
694            }
695            if !tz_set.contains(&a.timezone) {
696                tz_set.push(a.timezone.clone());
697            }
698        }
699
700        tz_set.sort();
701
702        Ok(ContinentStats {
703            total: airports.len(),
704            by_type,
705            by_country,
706            with_scheduled_service,
707            average_runway_length: if runway_count > 0 {
708                (runway_sum / runway_count as f64).round()
709            } else {
710                0.0
711            },
712            average_elevation: if elevation_count > 0 {
713                (elevation_sum / elevation_count as f64).round()
714            } else {
715                0.0
716            },
717            timezones: tz_set,
718        })
719    }
720
721    /// Gets the largest airports on a continent sorted by runway length or
722    /// elevation.
723    ///
724    /// `sort_by` can be `"runway"` (default) or `"elevation"`.
725    pub fn get_largest_airports_by_continent(
726        &self,
727        continent_code: &str,
728        limit: usize,
729        sort_by: &str,
730    ) -> Vec<Airport> {
731        let mut airports: Vec<Airport> = self
732            .get_airports_by_continent(continent_code)
733            .into_iter()
734            .cloned()
735            .collect();
736
737        match sort_by.to_lowercase().as_str() {
738            "elevation" => {
739                airports.sort_by(|a, b| {
740                    let ea = a.elevation_ft.unwrap_or(0);
741                    let eb = b.elevation_ft.unwrap_or(0);
742                    eb.cmp(&ea)
743                });
744            }
745            _ => {
746                // Default: sort by runway length
747                airports.sort_by(|a, b| {
748                    let ra = a.runway_length.unwrap_or(0);
749                    let rb = b.runway_length.unwrap_or(0);
750                    rb.cmp(&ra)
751                });
752            }
753        }
754
755        airports.truncate(limit);
756        airports
757    }
758
759    // =======================================================================
760    // Bulk Operations
761    // =======================================================================
762
763    /// Fetches multiple airports by their IATA or ICAO codes.
764    ///
765    /// Returns `None` for codes that are not found.
766    pub fn get_multiple_airports(&self, codes: &[&str]) -> Vec<Option<&Airport>> {
767        codes
768            .iter()
769            .map(|code| self.resolve_airport(code).ok())
770            .collect()
771    }
772
773    /// Calculates distances between all pairs of airports in a list.
774    ///
775    /// Requires at least 2 valid codes.
776    pub fn calculate_distance_matrix(&self, codes: &[&str]) -> Result<DistanceMatrix> {
777        if codes.len() < 2 {
778            return Err(AirportError::InvalidInput(
779                "At least 2 airport codes are required for a distance matrix".to_string(),
780            ));
781        }
782
783        let mut resolved: Vec<(&Airport, String)> = Vec::new();
784        for code in codes {
785            let airport = self.resolve_airport(code).map_err(|_| {
786                AirportError::NotFound(format!("Airport not found for code: {}", code))
787            })?;
788            resolved.push((airport, code.to_string()));
789        }
790
791        let airport_infos: Vec<AirportInfo> = resolved
792            .iter()
793            .map(|(a, code)| AirportInfo {
794                code: code.to_string(),
795                name: a.airport.clone(),
796                iata: a.iata.clone(),
797                icao: a.icao.clone(),
798            })
799            .collect();
800
801        let mut distances: HashMap<String, HashMap<String, f64>> = HashMap::new();
802        for (a1, code1) in &resolved {
803            let mut inner: HashMap<String, f64> = HashMap::new();
804            for (a2, code2) in &resolved {
805                if code1 == code2 {
806                    inner.insert(code2.clone(), 0.0);
807                } else {
808                    let dist =
809                        haversine_distance(a1.latitude, a1.longitude, a2.latitude, a2.longitude);
810                    inner.insert(code2.clone(), dist.round());
811                }
812            }
813            distances.insert(code1.clone(), inner);
814        }
815
816        Ok(DistanceMatrix {
817            airports: airport_infos,
818            distances,
819        })
820    }
821
822    // =======================================================================
823    // Validation & Utilities
824    // =======================================================================
825
826    /// Validates if an IATA code exists in the database.
827    ///
828    /// Code must be exactly 3 uppercase letters.
829    pub fn validate_iata_code(&self, code: &str) -> bool {
830        let trimmed = code.trim();
831        if trimmed.len() != 3 || !trimmed.chars().all(|c| c.is_ascii_uppercase()) {
832            return false;
833        }
834        AIRPORTS
835            .iter()
836            .any(|a| a.iata.eq_ignore_ascii_case(trimmed))
837    }
838
839    /// Validates if an ICAO code exists in the database.
840    ///
841    /// Code must be exactly 4 uppercase alphanumeric characters.
842    pub fn validate_icao_code(&self, code: &str) -> bool {
843        let trimmed = code.trim();
844        if trimmed.len() != 4
845            || !trimmed
846                .chars()
847                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
848        {
849            return false;
850        }
851        AIRPORTS
852            .iter()
853            .any(|a| a.icao.eq_ignore_ascii_case(trimmed))
854    }
855
856    /// Gets the count of airports matching the given filters.
857    ///
858    /// Pass `None` for total count of all airports.
859    pub fn get_airport_count(&self, filter: Option<&AirportFilter>) -> usize {
860        match filter {
861            Some(f) => AIRPORTS.iter().filter(|a| matches_filter(a, f)).count(),
862            None => AIRPORTS.len(),
863        }
864    }
865
866    /// Checks if an airport has scheduled commercial service.
867    ///
868    /// Accepts either IATA or ICAO code.
869    pub fn is_airport_operational(&self, code: &str) -> Result<bool> {
870        let airport = self.resolve_airport(code)?;
871        Ok(airport.scheduled_service)
872    }
873
874    // =======================================================================
875    // Internal helpers
876    // =======================================================================
877
878    /// Resolves an airport code (tries IATA first, then ICAO).
879    fn resolve_airport(&self, code: &str) -> Result<&Airport> {
880        let trimmed = code.trim();
881        // Try IATA first (3 chars)
882        if let Some(a) = AIRPORTS
883            .iter()
884            .find(|a| !a.iata.is_empty() && a.iata.eq_ignore_ascii_case(trimmed))
885        {
886            return Ok(a);
887        }
888        // Try ICAO
889        if let Some(a) = AIRPORTS
890            .iter()
891            .find(|a| !a.icao.is_empty() && a.icao.eq_ignore_ascii_case(trimmed))
892        {
893            return Ok(a);
894        }
895        Err(AirportError::NotFound(format!(
896            "No airport found for code: {}",
897            code
898        )))
899    }
900}
901
902impl Default for AirportData {
903    fn default() -> Self {
904        Self::new()
905    }
906}
907
908// ---------------------------------------------------------------------------
909// Tests
910// ---------------------------------------------------------------------------
911
912#[cfg(test)]
913mod tests {
914    use super::*;
915
916    fn db() -> AirportData {
917        AirportData::new()
918    }
919
920    // ===================================================================
921    // getAirportByIata
922    // ===================================================================
923    #[test]
924    fn test_get_airport_by_iata_lhr() {
925        let d = db();
926        let airport = d.get_airport_by_iata("LHR").unwrap();
927        assert_eq!(airport.iata, "LHR");
928        assert!(airport.airport.contains("Heathrow"));
929    }
930
931    // ===================================================================
932    // getAirportByIcao
933    // ===================================================================
934    #[test]
935    fn test_get_airport_by_icao_egll() {
936        let d = db();
937        let airport = d.get_airport_by_icao("EGLL").unwrap();
938        assert_eq!(airport.icao, "EGLL");
939        assert!(airport.airport.contains("Heathrow"));
940    }
941
942    // ===================================================================
943    // getAirportByCountryCode
944    // ===================================================================
945    #[test]
946    fn test_get_airports_by_country_code_us() {
947        let d = db();
948        let airports = d.get_airports_by_country_code("US");
949        assert!(airports.len() > 100);
950        assert_eq!(airports[0].country_code, "US");
951    }
952
953    // ===================================================================
954    // getAirportByContinent
955    // ===================================================================
956    #[test]
957    fn test_get_airports_by_continent_eu() {
958        let d = db();
959        let airports = d.get_airports_by_continent("EU");
960        assert!(airports.len() > 100);
961        assert!(airports.iter().all(|a| a.continent == "EU"));
962    }
963
964    // ===================================================================
965    // findNearbyAirports
966    // ===================================================================
967    #[test]
968    fn test_find_nearby_airports_london() {
969        let d = db();
970        let nearby = d.find_nearby_airports(51.5074, -0.1278, 50.0);
971        assert!(!nearby.is_empty());
972        assert!(nearby.iter().any(|n| n.airport.iata == "LHR"));
973    }
974
975    // ===================================================================
976    // getAirportsByType
977    // ===================================================================
978    #[test]
979    fn test_get_airports_by_type_large() {
980        let d = db();
981        let airports = d.get_airports_by_type("large_airport");
982        assert!(airports.len() > 10);
983        assert!(airports.iter().all(|a| a.airport_type == "large_airport"));
984    }
985
986    #[test]
987    fn test_get_airports_by_type_medium() {
988        let d = db();
989        let airports = d.get_airports_by_type("medium_airport");
990        assert!(airports.len() > 10);
991        assert!(airports.iter().all(|a| a.airport_type == "medium_airport"));
992    }
993
994    #[test]
995    fn test_get_airports_by_type_airport_generic() {
996        let d = db();
997        let airports = d.get_airports_by_type("airport");
998        assert!(airports.len() > 50);
999        assert!(airports.iter().all(|a| a.airport_type.contains("airport")));
1000    }
1001
1002    #[test]
1003    fn test_get_airports_by_type_heliport() {
1004        let d = db();
1005        let heliports = d.get_airports_by_type("heliport");
1006        for h in &heliports {
1007            assert_eq!(h.airport_type, "heliport");
1008        }
1009    }
1010
1011    #[test]
1012    fn test_get_airports_by_type_seaplane_base() {
1013        let d = db();
1014        let bases = d.get_airports_by_type("seaplane_base");
1015        for b in &bases {
1016            assert_eq!(b.airport_type, "seaplane_base");
1017        }
1018    }
1019
1020    #[test]
1021    fn test_get_airports_by_type_case_insensitive() {
1022        let d = db();
1023        let upper = d.get_airports_by_type("LARGE_AIRPORT");
1024        let lower = d.get_airports_by_type("large_airport");
1025        assert_eq!(upper.len(), lower.len());
1026        assert!(!upper.is_empty());
1027    }
1028
1029    #[test]
1030    fn test_get_airports_by_type_nonexistent() {
1031        let d = db();
1032        let airports = d.get_airports_by_type("nonexistent_type");
1033        assert!(airports.is_empty());
1034    }
1035
1036    // ===================================================================
1037    // getAutocompleteSuggestions
1038    // ===================================================================
1039    #[test]
1040    fn test_autocomplete_london() {
1041        let d = db();
1042        let suggestions = d.get_autocomplete_suggestions("London");
1043        assert!(!suggestions.is_empty());
1044        assert!(suggestions.len() <= 10);
1045        assert!(suggestions.iter().any(|a| a.iata == "LHR"));
1046    }
1047
1048    // ===================================================================
1049    // calculateDistance
1050    // ===================================================================
1051    #[test]
1052    fn test_calculate_distance_lhr_jfk() {
1053        let d = db();
1054        let dist = d.calculate_distance("LHR", "JFK").unwrap();
1055        // Approx 5541 km
1056        assert!((dist - 5541.0).abs() < 50.0);
1057    }
1058
1059    // ===================================================================
1060    // findAirports (Advanced Filtering)
1061    // ===================================================================
1062    #[test]
1063    fn test_find_airports_gb_airport() {
1064        let d = db();
1065        let filter = AirportFilter {
1066            country_code: Some("GB".to_string()),
1067            airport_type: Some("airport".to_string()),
1068            ..Default::default()
1069        };
1070        let airports = d.find_airports(&filter);
1071        // The "airport" type matches large_airport, medium_airport, small_airport
1072        // so the airport_type field will contain "airport" as a substring
1073        assert!(airports
1074            .iter()
1075            .all(|a| a.country_code == "GB" && a.airport_type.contains("airport")));
1076    }
1077
1078    #[test]
1079    fn test_find_airports_scheduled_service() {
1080        let d = db();
1081        let with_service = d.find_airports(&AirportFilter {
1082            has_scheduled_service: Some(true),
1083            ..Default::default()
1084        });
1085        let without_service = d.find_airports(&AirportFilter {
1086            has_scheduled_service: Some(false),
1087            ..Default::default()
1088        });
1089        assert!(with_service.len() + without_service.len() > 0);
1090
1091        if !with_service.is_empty() {
1092            assert!(with_service.iter().all(|a| a.scheduled_service));
1093        }
1094        if !without_service.is_empty() {
1095            assert!(without_service.iter().all(|a| !a.scheduled_service));
1096        }
1097    }
1098
1099    // ===================================================================
1100    // getAirportsByTimezone
1101    // ===================================================================
1102    #[test]
1103    fn test_get_airports_by_timezone_london() {
1104        let d = db();
1105        let airports = d.get_airports_by_timezone("Europe/London");
1106        assert!(airports.len() > 10);
1107        assert!(airports.iter().all(|a| a.timezone == "Europe/London"));
1108    }
1109
1110    // ===================================================================
1111    // getAirportLinks
1112    // ===================================================================
1113    #[test]
1114    fn test_get_airport_links_lhr() {
1115        let d = db();
1116        let links = d.get_airport_links("LHR").unwrap();
1117        assert!(links
1118            .wikipedia
1119            .as_deref()
1120            .unwrap_or("")
1121            .contains("Heathrow_Airport"));
1122        assert!(links.website.is_some());
1123    }
1124
1125    #[test]
1126    fn test_get_airport_links_hnd() {
1127        let d = db();
1128        let links = d.get_airport_links("HND").unwrap();
1129        assert!(links
1130            .wikipedia
1131            .as_deref()
1132            .unwrap_or("")
1133            .contains("Tokyo_International_Airport"));
1134        assert!(links.website.is_some());
1135    }
1136
1137    // ===================================================================
1138    // getAirportStatsByCountry
1139    // ===================================================================
1140    #[test]
1141    fn test_stats_by_country_sg() {
1142        let d = db();
1143        let stats = d.get_airport_stats_by_country("SG").unwrap();
1144        assert!(stats.total > 0);
1145        assert!(!stats.timezones.is_empty());
1146    }
1147
1148    #[test]
1149    fn test_stats_by_country_us() {
1150        let d = db();
1151        let stats = d.get_airport_stats_by_country("US").unwrap();
1152        assert!(stats.total > 1000);
1153        assert!(stats.by_type.contains_key("large_airport"));
1154        assert!(*stats.by_type.get("large_airport").unwrap() > 0);
1155    }
1156
1157    #[test]
1158    fn test_stats_by_country_invalid() {
1159        let d = db();
1160        let result = d.get_airport_stats_by_country("XYZ");
1161        assert!(result.is_err());
1162    }
1163
1164    // ===================================================================
1165    // getAirportStatsByContinent
1166    // ===================================================================
1167    #[test]
1168    fn test_stats_by_continent_as() {
1169        let d = db();
1170        let stats = d.get_airport_stats_by_continent("AS").unwrap();
1171        assert!(stats.total > 100);
1172        assert!(stats.by_country.len() > 10);
1173    }
1174
1175    #[test]
1176    fn test_stats_by_continent_eu() {
1177        let d = db();
1178        let stats = d.get_airport_stats_by_continent("EU").unwrap();
1179        assert!(stats.by_country.contains_key("GB"));
1180        assert!(stats.by_country.contains_key("FR"));
1181        assert!(stats.by_country.contains_key("DE"));
1182    }
1183
1184    // ===================================================================
1185    // getLargestAirportsByContinent
1186    // ===================================================================
1187    #[test]
1188    fn test_largest_by_continent_runway() {
1189        let d = db();
1190        let airports = d.get_largest_airports_by_continent("AS", 5, "runway");
1191        assert!(airports.len() <= 5);
1192        assert!(!airports.is_empty());
1193        for i in 0..airports.len() - 1 {
1194            let r1 = airports[i].runway_length.unwrap_or(0);
1195            let r2 = airports[i + 1].runway_length.unwrap_or(0);
1196            assert!(r1 >= r2);
1197        }
1198    }
1199
1200    #[test]
1201    fn test_largest_by_continent_elevation() {
1202        let d = db();
1203        let airports = d.get_largest_airports_by_continent("SA", 5, "elevation");
1204        assert!(airports.len() <= 5);
1205        for i in 0..airports.len() - 1 {
1206            let e1 = airports[i].elevation_ft.unwrap_or(0);
1207            let e2 = airports[i + 1].elevation_ft.unwrap_or(0);
1208            assert!(e1 >= e2);
1209        }
1210    }
1211
1212    #[test]
1213    fn test_largest_by_continent_respects_limit() {
1214        let d = db();
1215        let airports = d.get_largest_airports_by_continent("EU", 3, "runway");
1216        assert!(airports.len() <= 3);
1217    }
1218
1219    // ===================================================================
1220    // getMultipleAirports
1221    // ===================================================================
1222    #[test]
1223    fn test_get_multiple_airports_iata() {
1224        let d = db();
1225        let airports = d.get_multiple_airports(&["SIN", "LHR", "JFK"]);
1226        assert_eq!(airports.len(), 3);
1227        assert_eq!(airports[0].as_ref().unwrap().iata, "SIN");
1228        assert_eq!(airports[1].as_ref().unwrap().iata, "LHR");
1229        assert_eq!(airports[2].as_ref().unwrap().iata, "JFK");
1230    }
1231
1232    #[test]
1233    fn test_get_multiple_airports_mixed_codes() {
1234        let d = db();
1235        let airports = d.get_multiple_airports(&["SIN", "EGLL", "JFK"]);
1236        assert_eq!(airports.len(), 3);
1237        assert!(airports.iter().all(|a| a.is_some()));
1238    }
1239
1240    #[test]
1241    fn test_get_multiple_airports_with_invalid() {
1242        let d = db();
1243        let airports = d.get_multiple_airports(&["SIN", "INVALID", "LHR"]);
1244        assert_eq!(airports.len(), 3);
1245        assert!(airports[0].is_some());
1246        assert!(airports[1].is_none());
1247        assert!(airports[2].is_some());
1248    }
1249
1250    #[test]
1251    fn test_get_multiple_airports_empty() {
1252        let d = db();
1253        let airports = d.get_multiple_airports(&[]);
1254        assert!(airports.is_empty());
1255    }
1256
1257    // ===================================================================
1258    // calculateDistanceMatrix
1259    // ===================================================================
1260    #[test]
1261    fn test_distance_matrix() {
1262        let d = db();
1263        let matrix = d.calculate_distance_matrix(&["SIN", "LHR", "JFK"]).unwrap();
1264        assert_eq!(matrix.airports.len(), 3);
1265
1266        // Diagonal is zero
1267        assert_eq!(matrix.distances["SIN"]["SIN"], 0.0);
1268        assert_eq!(matrix.distances["LHR"]["LHR"], 0.0);
1269        assert_eq!(matrix.distances["JFK"]["JFK"], 0.0);
1270
1271        // Symmetry
1272        assert_eq!(
1273            matrix.distances["SIN"]["LHR"],
1274            matrix.distances["LHR"]["SIN"]
1275        );
1276        assert_eq!(
1277            matrix.distances["SIN"]["JFK"],
1278            matrix.distances["JFK"]["SIN"]
1279        );
1280
1281        // Reasonable distances
1282        assert!(matrix.distances["SIN"]["LHR"] > 5000.0);
1283        assert!(matrix.distances["LHR"]["JFK"] > 3000.0);
1284    }
1285
1286    #[test]
1287    fn test_distance_matrix_too_few() {
1288        let d = db();
1289        let result = d.calculate_distance_matrix(&["SIN"]);
1290        assert!(result.is_err());
1291    }
1292
1293    #[test]
1294    fn test_distance_matrix_invalid_code() {
1295        let d = db();
1296        let result = d.calculate_distance_matrix(&["SIN", "INVALID"]);
1297        assert!(result.is_err());
1298    }
1299
1300    // ===================================================================
1301    // findNearestAirport
1302    // ===================================================================
1303    #[test]
1304    fn test_find_nearest_airport_sin() {
1305        let d = db();
1306        let nearest = d.find_nearest_airport(1.35019, 103.994003, None).unwrap();
1307        assert_eq!(nearest.airport.iata, "SIN");
1308        assert!(nearest.distance < 2.0);
1309    }
1310
1311    #[test]
1312    fn test_find_nearest_airport_with_type_filter() {
1313        let d = db();
1314        let filter = AirportFilter {
1315            airport_type: Some("large_airport".to_string()),
1316            ..Default::default()
1317        };
1318        let nearest = d
1319            .find_nearest_airport(51.5074, -0.1278, Some(&filter))
1320            .unwrap();
1321        assert_eq!(nearest.airport.airport_type, "large_airport");
1322        assert!(nearest.distance > 0.0);
1323    }
1324
1325    #[test]
1326    fn test_find_nearest_airport_with_type_and_country() {
1327        let d = db();
1328        let filter = AirportFilter {
1329            airport_type: Some("large_airport".to_string()),
1330            country_code: Some("US".to_string()),
1331            ..Default::default()
1332        };
1333        let nearest = d
1334            .find_nearest_airport(40.7128, -74.0060, Some(&filter))
1335            .unwrap();
1336        assert_eq!(nearest.airport.airport_type, "large_airport");
1337        assert_eq!(nearest.airport.country_code, "US");
1338    }
1339
1340    // ===================================================================
1341    // validateIataCode
1342    // ===================================================================
1343    #[test]
1344    fn test_validate_iata_valid() {
1345        let d = db();
1346        assert!(d.validate_iata_code("SIN"));
1347        assert!(d.validate_iata_code("LHR"));
1348        assert!(d.validate_iata_code("JFK"));
1349    }
1350
1351    #[test]
1352    fn test_validate_iata_invalid() {
1353        let d = db();
1354        assert!(!d.validate_iata_code("XYZ"));
1355        assert!(!d.validate_iata_code("ZZZ"));
1356    }
1357
1358    #[test]
1359    fn test_validate_iata_bad_format() {
1360        let d = db();
1361        assert!(!d.validate_iata_code("ABCD"));
1362        assert!(!d.validate_iata_code("AB"));
1363        assert!(!d.validate_iata_code("abc"));
1364        assert!(!d.validate_iata_code(""));
1365    }
1366
1367    // ===================================================================
1368    // validateIcaoCode
1369    // ===================================================================
1370    #[test]
1371    fn test_validate_icao_valid() {
1372        let d = db();
1373        assert!(d.validate_icao_code("WSSS"));
1374        assert!(d.validate_icao_code("EGLL"));
1375        assert!(d.validate_icao_code("KJFK"));
1376    }
1377
1378    #[test]
1379    fn test_validate_icao_invalid() {
1380        let d = db();
1381        assert!(!d.validate_icao_code("XXXX"));
1382        assert!(!d.validate_icao_code("ZZZ0"));
1383    }
1384
1385    #[test]
1386    fn test_validate_icao_bad_format() {
1387        let d = db();
1388        assert!(!d.validate_icao_code("ABC"));
1389        assert!(!d.validate_icao_code("ABCDE"));
1390        assert!(!d.validate_icao_code("abcd"));
1391        assert!(!d.validate_icao_code(""));
1392    }
1393
1394    // ===================================================================
1395    // getAirportCount
1396    // ===================================================================
1397    #[test]
1398    fn test_get_airport_count_total() {
1399        let d = db();
1400        assert!(d.get_airport_count(None) > 5000);
1401    }
1402
1403    #[test]
1404    fn test_get_airport_count_by_type() {
1405        let d = db();
1406        let large_count = d.get_airport_count(Some(&AirportFilter {
1407            airport_type: Some("large_airport".to_string()),
1408            ..Default::default()
1409        }));
1410        let total = d.get_airport_count(None);
1411        assert!(large_count > 0);
1412        assert!(large_count < total);
1413    }
1414
1415    #[test]
1416    fn test_get_airport_count_by_country() {
1417        let d = db();
1418        let count = d.get_airport_count(Some(&AirportFilter {
1419            country_code: Some("US".to_string()),
1420            ..Default::default()
1421        }));
1422        assert!(count > 1000);
1423    }
1424
1425    #[test]
1426    fn test_get_airport_count_multiple_filters() {
1427        let d = db();
1428        let count = d.get_airport_count(Some(&AirportFilter {
1429            country_code: Some("US".to_string()),
1430            airport_type: Some("large_airport".to_string()),
1431            ..Default::default()
1432        }));
1433        assert!(count > 0);
1434        assert!(count < 200);
1435    }
1436
1437    // ===================================================================
1438    // isAirportOperational
1439    // ===================================================================
1440    #[test]
1441    fn test_is_airport_operational_true() {
1442        let d = db();
1443        assert!(d.is_airport_operational("SIN").unwrap());
1444        assert!(d.is_airport_operational("LHR").unwrap());
1445        assert!(d.is_airport_operational("JFK").unwrap());
1446    }
1447
1448    #[test]
1449    fn test_is_airport_operational_both_codes() {
1450        let d = db();
1451        assert!(d.is_airport_operational("SIN").unwrap());
1452        assert!(d.is_airport_operational("WSSS").unwrap());
1453    }
1454
1455    #[test]
1456    fn test_is_airport_operational_invalid() {
1457        let d = db();
1458        let result = d.is_airport_operational("INVALID");
1459        assert!(result.is_err());
1460    }
1461
1462    // ===================================================================
1463    // searchByName
1464    // ===================================================================
1465    #[test]
1466    fn test_search_by_name() {
1467        let d = db();
1468        let results = d.search_by_name("Singapore").unwrap();
1469        assert!(!results.is_empty());
1470    }
1471}