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(¶ms).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(¶ms).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}