/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis *file,Youcanobtainoneathttp://mozilla.org/MPL/2.0/.
*/
/// GeoNames support. GeoNames is an open-source geographical database of place /// names worldwide, including cities, regions, and countries [1]. Notably it's /// used by MaxMind's databases [2]. We use GeoNames to detect city and region /// names and to map cities to regions. /// /// [1]: https://www.geonames.org/ /// [2]: https://www.maxmind.com/en/geoip-databases use rusqlite::{named_params, Connection}; use serde::Deserialize; use sql_support::ConnExt; use std::hash::{Hash, Hasher};
/// The type of a geoname. #[derive(Clone, Debug, Eq, Hash, PartialEq, uniffi::Enum)] pubenum GeonameType {
City,
Region,
}
/// A single geographic place. /// /// This corresponds to a single row in the main "geoname" table described in /// the GeoNames documentation [1]. We exclude fields we don't need. /// /// [1]: https://download.geonames.org/export/dump/readme.txt #[derive(Clone, Debug, uniffi::Record)] pubstruct Geoname { /// The `geonameid` straight from the geoname table. pub geoname_id: i64, /// This is pretty much the place's canonical name. Usually there will be a /// row in the alternates table with the same name, but not always. When /// there is such a row, it doesn't always have `is_preferred_name` set, and /// in fact fact there may be another row with a different name with /// `is_preferred_name` set. pub name: String, /// Latitude in decimal degrees. pub latitude: f64, /// Longitude in decimal degrees. pub longitude: f64, /// ISO-3166 two-letter uppercase country code, e.g., "US". pub country_code: String, /// The top-level administrative region for the place within its country, /// like a state or province. For the U.S., the two-letter uppercase state /// abbreviation. pub admin1_code: String, /// Population size. pub population: u64,
}
impl Geoname { /// Whether `self` and `other` have the same region and country. If one is a /// city and the other is a region, this will return `true` if the city is /// located in the region. pubfn has_same_region(&self, other: &Self) -> bool { self.admin1_code == other.admin1_code && self.country_code == other.country_code
}
}
/// A fetched geoname with info on how it was matched. #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] pubstruct GeonameMatch { /// The geoname that was matched. pub geoname: Geoname, /// The type of name that was matched. pub match_type: GeonameMatchType, /// Whether the name was matched by prefix. pub prefix: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, uniffi::Enum)] pubenum GeonameMatchType { /// For U.S. states, abbreviations are the usual two-letter codes ("CA").
Abbreviation,
AirportCode, /// This includes any names that aren't abbreviations or airport codes.
Name,
}
/// This data is used to service every query handled by the weather provider and /// potentially other providers, so we cache it from the DB. #[derive(Debug, Default)] pubstruct GeonameCache { /// Max length of all geoname names. pub max_name_length: usize, /// Max word count across all geoname names. pub max_name_word_count: usize,
}
#[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedGeonameAttachment { /// The max length of all names in the attachment. Used for name metrics. We /// pre-compute this to avoid doing duplicate work on all user's machines. pub max_alternate_name_length: u32, /// The max word count across all names in the attachment. Used for name /// metrics. We pre-compute this to avoid doing duplicate work on all user's /// machines. pub max_alternate_name_word_count: u32, pub geonames: Vec<DownloadedGeoname>,
}
/// This corresponds to a single row in the main "geoname" table described in /// the GeoNames documentation [1] except where noted. It represents a single /// place. We exclude fields we don't need. /// /// [1] https://download.geonames.org/export/dump/readme.txt #[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedGeoname { /// The `geonameid` straight from the geoname table. pub id: i64, /// NOTE: For ease of implementation, this name should always also be /// included as a lowercased alternate name even if the original GeoNames /// data doesn't include it as an alternate. pub name: String, /// "P" - Populated place like a city or village. /// "A" - Administrative division like a country, state, or region. pub feature_class: String, /// "ADM1" - Primary administrative division like a U.S. state. pub feature_code: String, /// ISO-3166 two-letter uppercase country code, e.g., "US". pub country_code: String, /// For the U.S., the two-letter uppercase state abbreviation. pub admin1_code: String, /// This can be helpful for resolving name conflicts. If two geonames have /// the same name, we might prefer the one with the larger population. pub population: u64, /// Latitude in decimal degrees. Expected to be a string in the RS data. #[serde(deserialize_with = "deserialize_f64_or_default")] pub latitude: f64, /// Longitude in decimal degrees. Expected to be a string in the RS data. #[serde(deserialize_with = "deserialize_f64_or_default")] pub longitude: f64, /// List of names that the place is known by. Despite the word "alternate", /// this often includes the place's proper name. This list is pulled from /// the "alternate names" table described in the GeoNames documentation and /// included here inline. /// /// NOTE: For ease of implementation, this list should always include a /// lowercase version of `name` even if the original GeoNames record doesn't /// include it as an alternate. /// /// Version 1 of this field was a `Vec<String>`. pub alternate_names_2: Vec<DownloadedGeonameAlternate>,
}
#[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedGeonameAlternate { /// Lowercase alternate name.
name: String, /// The value of the `iso_language` field for the alternate. This will be /// `None` for the alternate we artificially create for the `name` in the /// corresponding geoname record.
iso_language: Option<String>,
}
impl SuggestDao<'_> { /// Fetches geonames that have at least one name matching the `query` /// string. /// /// `match_name_prefix` determines whether prefix matching is performed on /// names that aren't abbreviations and airport codes. When `true`, names /// that start with `query` will match. When false, names that equal `query` /// will match. Prefix matching is never performed on abbreviations and /// airport codes because we don't currently have a use case for that. /// /// `geoname_type` restricts returned geonames to the specified type. `None` /// restricts geonames to cities and regions. There's no way to return /// geonames of other types, but we shouldn't ingest other types to begin /// with. /// /// `filter` restricts returned geonames to certain cities or regions. /// Cities can be restricted to certain regions by including the regions in /// `filter`, and regions can be restricted to those containing certain /// cities by including the cities in `filter`. This is especially useful /// since city and region names are not unique. `filter` is disjunctive: If /// any item in `filter` matches a geoname, the geoname will be filtered in. /// If `filter` is empty, all geonames will be filtered out. /// /// The returned matches will include all matching types for a geoname, one /// match per type per geoname. For example, if the query matches both a /// geoname's name and abbreviation, two matches for that geoname will be /// returned: one with a `match_type` of `GeonameMatchType::Name` and one /// with a `match_type` of `GeonameMatchType::Abbreviation`. `prefix` is set /// according to whether the query matched a prefix of the given type. pubfn fetch_geonames(
&self,
query: &str,
match_name_prefix: bool,
geoname_type: Option<GeonameType>,
filter: Option<Vec<&Geoname>>,
) -> Result<Vec<GeonameMatch>> { let city_pred = "(g.feature_class = 'P')"; let region_pred = "(g.feature_class = 'A' AND g.feature_code = 'ADM1')"; let type_pred = match geoname_type {
None => format!("({} OR {})", city_pred, region_pred),
Some(GeonameType::City) => city_pred.to_string(),
Some(GeonameType::Region) => region_pred.to_string(),
};
Ok(self
.conn
.query_rows_and_then_cached(
&format!(
r#"
SELECT
g.id,
g.name,
g.latitude,
g.longitude,
g.feature_class,
g.country_code,
g.admin1_code,
g.population,
a.name != :name AS prefix,
(SELECT CASE
-- abbreviation
WHEN a.iso_language = 'abbr' THEN 1
-- airport code
WHEN a.iso_language IN ('iata', 'icao', 'faac') THEN 2
-- name ELSE3
END
) AS match_type
FROM
geonames g
JOIN
geonames_alternates a ON g.id = a.geoname_id WHERE
{}
AND CASE :prefix
WHEN FALSE THEN a.name = :name ELSE (a.name = :name OR (
(a.name BETWEEN :name AND :name || X'FFFF')
AND match_type = 3
))
END
GROUP BY
g.id, match_type
ORDER BY
g.feature_class = 'P' DESC, g.population DESC, g.id ASC, a.iso_language ASC "#,
type_pred
),
named_params! { ":name": query.to_lowercase(), ":prefix": match_name_prefix,
},
|row| -> Result<Option<GeonameMatch>> { let g_match = GeonameMatch {
geoname: Geoname {
geoname_id: row.get("id")?,
name: row.get("name")?,
latitude: row.get("latitude")?,
longitude: row.get("longitude")?,
country_code: row.get("country_code")?,
admin1_code: row.get("admin1_code")?,
population: row.get("population")?,
},
prefix: row.get("prefix")?,
match_type: match row.get::<_, i32>("match_type")? { 1 => GeonameMatchType::Abbreviation, 2 => GeonameMatchType::AirportCode,
_ => GeonameMatchType::Name,
},
}; iflet Some(geonames) = &filter {
geonames
.iter()
.find(|g| g.has_same_region(&g_match.geoname))
.map(|_| Ok(Some(g_match)))
.unwrap_or(Ok(None))
} else {
Ok(Some(g_match))
}
},
)?
.into_iter()
.flatten()
.collect())
}
/// Inserts GeoNames data into the database. fn insert_geonames(
&mutself,
record_id: &SuggestRecordId,
attachments: &[DownloadedGeonameAttachment],
) -> Result<()> { self.scope.err_if_interrupted()?; letmut geoname_insert = GeonameInsertStatement::new(self.conn)?; letmut alt_insert = GeonameAlternateInsertStatement::new(self.conn)?; letmut metrics_insert = GeonameMetricsInsertStatement::new(self.conn)?; letmut max_len = 0; letmut max_word_count = 0; for attach in attachments { for geoname in &attach.geonames {
geoname_insert.execute(record_id, geoname)?; for alt in &geoname.alternate_names_2 {
alt_insert.execute(alt, geoname.id)?;
}
}
max_len = std::cmp::max(max_len, attach.max_alternate_name_length as usize);
max_word_count = std::cmp::max(
max_word_count,
attach.max_alternate_name_word_count as usize,
);
}
// We just made some insertions that might invalidate the data in the // cache. Clear it so it's repopulated the next time it's accessed. self.geoname_cache.take();
// Add a couple of records with different metrics. We're just testing // metrics so the other values don't matter. letmut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record( "geonames", "geonames-0",
json!({ "max_alternate_name_length": 10, "max_alternate_name_word_count": 5, "geonames": []
}),
)
.with_record( "geonames", "geonames-1",
json!({ "max_alternate_name_length": 20, "max_alternate_name_word_count": 2, "geonames": []
}),
),
);
// Ingest weather to also ingest geonames.
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Weather]),
..SuggestIngestionConstraints::all_providers()
});
// Create the store with the test data and ingest. letmut store = new_test_store();
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Weather]),
..SuggestIngestionConstraints::all_providers()
});
// Make sure we have a match.
store.read(|dao| {
assert_eq!(
dao.fetch_geonames("waterloo", false, None, None)?,
vec![
GeonameMatch {
geoname: waterloo_ia(),
match_type: GeonameMatchType::Name,
prefix: false,
},
GeonameMatch {
geoname: waterloo_al(),
match_type: GeonameMatchType::Name,
prefix: false,
},
],
);
Ok(())
})?;
// Delete the record.
store
.client_mut()
.delete_record("quicksuggest", "geonames-0");
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Weather]),
..SuggestIngestionConstraints::all_providers()
});
// The same query shouldn't match anymore and the tables should be // empty.
store.read(|dao| {
assert_eq!(dao.fetch_geonames("waterloo", false, None, None)?, vec![],);
let g_ids = dao.conn.query_rows_and_then( "SELECT id FROM geonames",
[],
|row| -> Result<i64> { Ok(row.get("id")?) },
)?;
assert_eq!(g_ids, Vec::<i64>::new());
let alt_g_ids = dao.conn.query_rows_and_then( "SELECT geoname_id FROM geonames_alternates",
[],
|row| -> Result<i64> { Ok(row.get("geoname_id")?) },
)?;
assert_eq!(alt_g_ids, Vec::<i64>::new());
// This only tests a few different calls to exercise all the fetch // options. Comprehensive fetch cases are in the main `geonames` test. let tests = [ // simple fetch with no options
Test {
query: "ia",
match_name_prefix: false,
geoname_type: None,
filter: None,
expected: vec![GeonameMatch {
geoname: ia(),
match_type: GeonameMatchType::Abbreviation,
prefix: false,
}],
}, // filter
Test {
query: "ia",
match_name_prefix: false,
geoname_type: None,
filter: Some(vec![waterloo_ia(), waterloo_al()]),
expected: vec![GeonameMatch {
geoname: ia(),
match_type: GeonameMatchType::Abbreviation,
prefix: false,
}],
}, // geoname type: city
Test {
query: "ia",
match_name_prefix: false,
geoname_type: Some(GeonameType::Region),
filter: None,
expected: vec![GeonameMatch {
geoname: ia(),
match_type: GeonameMatchType::Abbreviation,
prefix: false,
}],
}, // geoname type: region
Test {
query: "ny",
match_name_prefix: false,
geoname_type: Some(GeonameType::City),
filter: None,
expected: vec![GeonameMatch {
geoname: nyc(),
match_type: GeonameMatchType::Abbreviation,
prefix: false,
}],
}, // prefix matching
Test {
query: "ny",
match_name_prefix: true,
geoname_type: None,
filter: None,
expected: vec![
GeonameMatch {
geoname: nyc(),
match_type: GeonameMatchType::Abbreviation,
prefix: false,
},
GeonameMatch {
geoname: ny_state(),
match_type: GeonameMatchType::Abbreviation,
prefix: false,
},
],
},
];
for t in tests {
assert_eq!(
store.fetch_geonames(
t.query,
t.match_name_prefix,
t.geoname_type.clone(),
t.filter.clone()
),
t.expected, "Test: {:?}",
t
);
}
Ok(())
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.27 Sekunden
(vorverarbeitet am 2026-06-18)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.