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