geocoding_async/
openstreetmap.rs

1//! The [OpenStreetMap Nominatim](https://nominatim.org/) provider.
2//!
3//! Geocoding methods are implemented on the [`Openstreetmap`](struct.Openstreetmap.html) struct.
4//! Please see the [API documentation](https://nominatim.org/release-docs/develop/) for details.
5//!
6//! While OpenStreetMap's Nominatim API is free, see the [Nominatim Usage Policy](https://operations.osmfoundation.org/policies/nominatim/)
7//! for details on usage requirements, including a maximum of 1 request per second.
8//!
9//! ### Example
10//!
11//! ```
12//! # tokio_test::block_on(async {
13//! use geocoding_async::{Openstreetmap, Forward, Point};
14//!
15//! let osm = Openstreetmap::new();
16//! let address = "Schwabing, München";
17//! let res = osm.forward(&address).await;
18//! assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]);
19//! # });
20//! ```
21use crate::GeocodingError;
22use crate::InputBounds;
23use crate::Point;
24use crate::UA_STRING;
25use crate::{Client, HeaderMap, HeaderValue, USER_AGENT};
26use crate::{Deserialize, Serialize};
27use crate::{Forward, Reverse};
28use num_traits::Float;
29use std::fmt::Debug;
30
31/// An instance of the Openstreetmap geocoding service
32pub struct Openstreetmap {
33    client: Client,
34    endpoint: String,
35}
36
37/// An instance of a parameter builder for Openstreetmap geocoding
38pub struct OpenstreetmapParams<'a, T>
39where
40    T: Float + Debug,
41{
42    query: &'a str,
43    addressdetails: bool,
44    viewbox: Option<&'a InputBounds<T>>,
45}
46
47impl<'a, T> OpenstreetmapParams<'a, T>
48where
49    T: Float + Debug,
50{
51    /// Create a new OpenStreetMap parameter builder
52    /// # Example:
53    ///
54    /// ```
55    /// use geocoding_async::{Openstreetmap, InputBounds, Point};
56    /// use geocoding_async::openstreetmap::{OpenstreetmapParams};
57    ///
58    /// let viewbox = InputBounds::new(
59    ///     (-0.13806939125061035, 51.51989264641164),
60    ///     (-0.13427138328552246, 51.52319711775629),
61    /// );
62    /// let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis")
63    ///     .with_addressdetails(true)
64    ///     .with_viewbox(&viewbox)
65    ///     .build();
66    /// ```
67    pub fn new(query: &'a str) -> OpenstreetmapParams<'a, T> {
68        OpenstreetmapParams {
69            query,
70            addressdetails: false,
71            viewbox: None,
72        }
73    }
74
75    /// Set the `addressdetails` property
76    pub fn with_addressdetails(&mut self, addressdetails: bool) -> &mut Self {
77        self.addressdetails = addressdetails;
78        self
79    }
80
81    /// Set the `viewbox` property
82    pub fn with_viewbox(&mut self, viewbox: &'a InputBounds<T>) -> &mut Self {
83        self.viewbox = Some(viewbox);
84        self
85    }
86
87    /// Build and return an instance of OpenstreetmapParams
88    pub fn build(&self) -> OpenstreetmapParams<'a, T> {
89        OpenstreetmapParams {
90            query: self.query,
91            addressdetails: self.addressdetails,
92            viewbox: self.viewbox,
93        }
94    }
95}
96
97impl Openstreetmap {
98    /// Create a new Openstreetmap geocoding instance using the default endpoint
99    pub fn new() -> Self {
100        Openstreetmap::new_with_endpoint("https://nominatim.openstreetmap.org/".to_string())
101    }
102
103    /// Create a new Openstreetmap geocoding instance with a custom endpoint.
104    ///
105    /// Endpoint should include a trailing slash (i.e. "https://nominatim.openstreetmap.org/")
106    pub fn new_with_endpoint(endpoint: String) -> Self {
107        let mut headers = HeaderMap::new();
108        headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING));
109        let client = Client::builder()
110            .default_headers(headers)
111            .build()
112            .expect("Couldn't build a client!");
113        Openstreetmap { client, endpoint }
114    }
115
116    /// A forward-geocoding lookup of an address, returning a full detailed response
117    ///
118    /// Accepts an [`OpenstreetmapParams`](struct.OpenstreetmapParams.html) struct for specifying
119    /// options, including whether to include address details in the response and whether to filter
120    /// by a bounding box.
121    ///
122    /// Please see [the documentation](https://nominatim.org/release-docs/develop/api/Search/) for details.
123    ///
124    /// This method passes the `format` parameter to the API.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// # tokio_test::block_on(async {
130    /// use geocoding_async::{Openstreetmap, InputBounds, Point};
131    /// use geocoding_async::openstreetmap::{OpenstreetmapParams, OpenstreetmapResponse};
132    ///
133    /// let osm = Openstreetmap::new();
134    /// let viewbox = InputBounds::new(
135    ///     (-0.13806939125061035, 51.51989264641164),
136    ///     (-0.13427138328552246, 51.52319711775629),
137    /// );
138    /// let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis")
139    ///     .with_addressdetails(true)
140    ///     .with_viewbox(&viewbox)
141    ///     .build();
142    /// let res: OpenstreetmapResponse<f64> = osm.forward_full(&params).await.unwrap();
143    /// let result = res.features[0].properties.clone();
144    /// assert!(result.display_name.contains("Tottenham Court Road"));
145    /// # });
146    /// ```
147    pub async fn forward_full<T>(
148        &self,
149        params: &OpenstreetmapParams<'_, T>,
150    ) -> Result<OpenstreetmapResponse<T>, GeocodingError>
151    where
152        T: Float + Debug,
153        for<'de> T: Deserialize<'de>,
154    {
155        let format = String::from("geojson");
156        let addressdetails = String::from(if params.addressdetails { "1" } else { "0" });
157        // For lifetime issues
158        let viewbox;
159
160        let mut query = vec![
161            (&"q", params.query),
162            (&"format", &format),
163            (&"addressdetails", &addressdetails),
164        ];
165
166        if let Some(vb) = params.viewbox {
167            viewbox = String::from(*vb);
168            query.push((&"viewbox", &viewbox));
169        }
170
171        let resp = self
172            .client
173            .get(&format!("{}search", self.endpoint))
174            .query(&query)
175            .send()
176            .await?
177            .error_for_status()?;
178        let res: OpenstreetmapResponse<T> = resp.json().await?;
179        Ok(res)
180    }
181}
182
183impl Default for Openstreetmap {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189impl<T> Forward<T> for Openstreetmap
190where
191    T: Float + Debug,
192    for<'de> T: Deserialize<'de>,
193{
194    /// A forward-geocoding lookup of an address. Please see [the documentation](https://nominatim.org/release-docs/develop/api/Search/) for details.
195    ///
196    /// This method passes the `format` parameter to the API.
197    async fn forward(&self, place: &str) -> Result<Vec<Point<T>>, GeocodingError> {
198        let resp = self
199            .client
200            .get(&format!("{}search", self.endpoint))
201            .query(&[(&"q", place), (&"format", &String::from("geojson"))])
202            .send()
203            .await?
204            .error_for_status()?;
205        let res: OpenstreetmapResponse<T> = resp.json().await?;
206        Ok(res
207            .features
208            .iter()
209            .map(|res| Point::new(res.geometry.coordinates.0, res.geometry.coordinates.1))
210            .collect())
211    }
212}
213
214impl<T> Reverse<T> for Openstreetmap
215where
216    T: Float + Debug,
217    for<'de> T: Deserialize<'de>,
218{
219    /// A reverse lookup of a point. More detail on the format of the
220    /// returned `String` can be found [here](https://nominatim.org/release-docs/develop/api/Reverse/)
221    ///
222    /// This method passes the `format` parameter to the API.
223    async fn reverse(&self, point: &Point<T>) -> Result<Option<String>, GeocodingError> {
224        let resp = self
225            .client
226            .get(&format!("{}reverse", self.endpoint))
227            .query(&[
228                (&"lon", &point.x().to_f64().unwrap().to_string()),
229                (&"lat", &point.y().to_f64().unwrap().to_string()),
230                (&"format", &String::from("geojson")),
231            ])
232            .send()
233            .await?
234            .error_for_status()?;
235        let res: OpenstreetmapResponse<T> = resp.json().await?;
236        let address = &res.features[0];
237        Ok(Some(address.properties.display_name.to_string()))
238    }
239}
240
241/// The top-level full GeoJSON response returned by a forward-geocoding request
242///
243/// See [the documentation](https://nominatim.org/release-docs/develop/api/Search/#geojson) for more details
244///
245///```json
246///{
247///  "type": "FeatureCollection",
248///  "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
249///  "features": [
250///    {
251///      "type": "Feature",
252///      "properties": {
253///        "place_id": 263681481,
254///        "osm_type": "way",
255///        "osm_id": 355421084,
256///        "display_name": "68, Carrer de Calatrava, les Tres Torres, Sarrià - Sant Gervasi, Barcelona, BCN, Catalonia, 08017, Spain",
257///        "place_rank": 30,
258///        "category": "building",
259///        "type": "apartments",
260///        "importance": 0.7409999999999999,
261///        "address": {
262///          "house_number": "68",
263///          "road": "Carrer de Calatrava",
264///          "suburb": "les Tres Torres",
265///          "city_district": "Sarrià - Sant Gervasi",
266///          "city": "Barcelona",
267///          "county": "BCN",
268///          "state": "Catalonia",
269///          "postcode": "08017",
270///          "country": "Spain",
271///          "country_code": "es"
272///        }
273///      },
274///      "bbox": [
275///        2.1284918,
276///        41.401227,
277///        2.128952,
278///        41.4015815
279///      ],
280///      "geometry": {
281///        "type": "Point",
282///        "coordinates": [
283///          2.12872241167437,
284///          41.40140675
285///        ]
286///      }
287///    }
288///  ]
289///}
290///```
291#[derive(Debug, Serialize, Deserialize)]
292pub struct OpenstreetmapResponse<T>
293where
294    T: Float + Debug,
295{
296    pub r#type: String,
297    pub licence: String,
298    pub features: Vec<OpenstreetmapResult<T>>,
299}
300
301/// A geocoding result
302#[derive(Debug, Serialize, Deserialize)]
303pub struct OpenstreetmapResult<T>
304where
305    T: Float + Debug,
306{
307    pub r#type: String,
308    pub properties: ResultProperties,
309    pub bbox: (T, T, T, T),
310    pub geometry: ResultGeometry<T>,
311}
312
313/// Geocoding result properties
314#[derive(Clone, Debug, Serialize, Deserialize)]
315pub struct ResultProperties {
316    pub place_id: u64,
317    pub osm_type: String,
318    pub osm_id: u64,
319    pub display_name: String,
320    pub place_rank: u64,
321    pub category: String,
322    pub r#type: String,
323    pub importance: f64,
324    pub address: Option<AddressDetails>,
325}
326
327/// Address details in the result object
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub struct AddressDetails {
330    pub city: Option<String>,
331    pub city_district: Option<String>,
332    pub construction: Option<String>,
333    pub continent: Option<String>,
334    pub country: Option<String>,
335    pub country_code: Option<String>,
336    pub house_number: Option<String>,
337    pub neighbourhood: Option<String>,
338    pub postcode: Option<String>,
339    pub public_building: Option<String>,
340    pub state: Option<String>,
341    pub suburb: Option<String>,
342    pub road: Option<String>,
343    pub village: Option<String>,
344}
345
346/// A geocoding result geometry
347#[derive(Debug, Serialize, Deserialize)]
348pub struct ResultGeometry<T>
349where
350    T: Float + Debug,
351{
352    pub r#type: String,
353    pub coordinates: (T, T),
354}
355
356#[cfg(test)]
357mod test {
358    use super::*;
359
360    #[tokio::test]
361    async fn new_with_endpoint_forward_test() {
362        let osm =
363            Openstreetmap::new_with_endpoint("https://nominatim.openstreetmap.org/".to_string());
364        let address = "Schwabing, München";
365        let res = osm.forward(&address).await;
366        assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]);
367    }
368
369    #[tokio::test]
370    async fn forward_full_test() {
371        let osm = Openstreetmap::new();
372        let viewbox = InputBounds::new(
373            (-0.13806939125061035, 51.51989264641164),
374            (-0.13427138328552246, 51.52319711775629),
375        );
376        let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis")
377            .with_addressdetails(true)
378            .with_viewbox(&viewbox)
379            .build();
380        let res: OpenstreetmapResponse<f64> = osm.forward_full(&params).await.unwrap();
381        let result = res.features[0].properties.clone();
382        assert!(result.display_name.contains("Tottenham Court Road"));
383        assert_eq!(result.address.unwrap().city.unwrap(), "London");
384    }
385
386    #[tokio::test]
387    async fn forward_test() {
388        let osm = Openstreetmap::new();
389        let address = "Schwabing, München";
390        let res = osm.forward(&address).await;
391        assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]);
392    }
393
394    #[tokio::test]
395    async fn reverse_test() {
396        let osm = Openstreetmap::new();
397        let p = Point::new(2.12870, 41.40139);
398        let res = osm.reverse(&p).await;
399        assert!(res
400            .unwrap()
401            .unwrap()
402            .contains("Barcelona, Barcelonès, Barcelona, Catalunya"));
403    }
404}