geocoding/opencage.rs
1//! The [OpenCage Geocoding](https://opencagedata.com/) provider.
2//!
3//! Geocoding methods are implemented on the [`Opencage`](struct.Opencage.html) struct.
4//! Please see the [API documentation](https://opencagedata.com/api) for details.
5//! Note that rate limits apply to the free tier:
6//! there is a [rate-limit](https://opencagedata.com/api#rate-limiting) of 1 request per second,
7//! and a quota of calls allowed per 24-hour period. The remaining daily quota can be retrieved
8//! using the [`remaining_calls()`](struct.Opencage.html#method.remaining_calls) method. If you
9//! are a paid tier user, this value will not be updated, and will remain `None`.
10//! ### A Note on Coordinate Order
11//! This provider's API documentation shows all coordinates in `[Latitude, Longitude]` order.
12//! However, `Geocoding` requires input `Point` coordinate order as `[Longitude, Latitude]`
13//! `(x, y)`, and returns coordinates with that order.
14//!
15//! ### Example
16//!
17//! ```
18//! use geocoding::{Opencage, Point, Reverse};
19//!
20//! let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
21//! oc.parameters.language = Some("fr");
22//! let p = Point::new(2.12870, 41.40139);
23//! let res = oc.reverse(&p);
24//! // "Carrer de Calatrava, 68, 08017 Barcelone, Espagne"
25//! println!("{:?}", res.unwrap());
26//! ```
27use crate::chrono::naive::serde::ts_seconds::deserialize as from_ts;
28use crate::chrono::NaiveDateTime;
29use crate::DeserializeOwned;
30use crate::GeocodingError;
31use crate::InputBounds;
32use crate::Point;
33use crate::UA_STRING;
34use crate::{Client, HeaderMap, HeaderValue, USER_AGENT};
35use crate::{Deserialize, Serialize};
36use crate::{Forward, Reverse};
37use num_traits::Float;
38use serde::Deserializer;
39use std::collections::HashMap;
40use std::fmt::Debug;
41use std::sync::{Arc, Mutex};
42
43macro_rules! add_optional_param {
44 ($query:expr, $param:expr, $name:expr) => {
45 if let Some(p) = $param {
46 $query.push(($name, p))
47 }
48 };
49}
50
51// Please see the [API documentation](https://opencagedata.com/api#forward-opt) for details.
52#[derive(Default)]
53pub struct Parameters<'a> {
54 pub language: Option<&'a str>,
55 pub countrycode: Option<&'a str>,
56 pub limit: Option<&'a str>,
57}
58
59impl<'a> Parameters<'a> {
60 fn as_query(&self) -> Vec<(&'a str, &'a str)> {
61 let mut query = vec![];
62 add_optional_param!(query, self.language, "language");
63 add_optional_param!(query, self.countrycode, "countrycode");
64 add_optional_param!(query, self.limit, "limit");
65 query
66 }
67}
68
69pub fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error>
70where
71 D: Deserializer<'de>,
72{
73 #[derive(Deserialize)]
74 #[serde(untagged)]
75 enum StringOrInt {
76 String(String),
77 Int(i32),
78 }
79
80 match StringOrInt::deserialize(deserializer)? {
81 StringOrInt::String(s) => Ok(s),
82 StringOrInt::Int(i) => Ok(i.to_string()),
83 }
84}
85
86// OpenCage has a custom rate-limit header, indicating remaining calls
87// header! { (XRatelimitRemaining, "X-RateLimit-Remaining") => [i32] }
88static XRL: &str = "x-ratelimit-remaining";
89/// Use this constant if you don't need to restrict a `forward_full` call with a bounding box
90pub static NOBOX: Option<InputBounds<f64>> = None::<InputBounds<f64>>;
91
92/// An instance of the Opencage Geocoding service
93pub struct Opencage<'a> {
94 api_key: String,
95 client: Client,
96 endpoint: String,
97 pub parameters: Parameters<'a>,
98 remaining: Arc<Mutex<Option<i32>>>,
99}
100
101impl<'a> Opencage<'a> {
102 /// Create a new OpenCage geocoding instance
103 pub fn new(api_key: String) -> Self {
104 let mut headers = HeaderMap::new();
105 headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING));
106 let client = Client::builder()
107 .default_headers(headers)
108 .build()
109 .expect("Couldn't build a client!");
110
111 let parameters = Parameters::default();
112 Opencage {
113 api_key,
114 client,
115 parameters,
116 endpoint: "https://api.opencagedata.com/geocode/v1/json".to_string(),
117 remaining: Arc::new(Mutex::new(None)),
118 }
119 }
120 /// Retrieve the remaining API calls in your daily quota
121 ///
122 /// Initially, this value is `None`. Any OpenCage API call using a "Free Tier" key
123 /// will update this value to reflect the remaining quota for the API key.
124 /// See the [API docs](https://opencagedata.com/api#rate-limiting) for details.
125 pub fn remaining_calls(&self) -> Option<i32> {
126 *self.remaining.lock().unwrap()
127 }
128 /// A reverse lookup of a point, returning an annotated response.
129 ///
130 /// This method passes the `no_record` parameter to the API.
131 ///
132 /// # Examples
133 ///
134 ///```
135 /// use geocoding::{Opencage, Point};
136 ///
137 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
138 /// let p = Point::new(2.12870, 41.40139);
139 /// // a full `OpencageResponse` struct
140 /// let res = oc.reverse_full(&p).unwrap();
141 /// // responses may include multiple results
142 /// let first_result = &res.results[0];
143 /// assert_eq!(
144 /// first_result.components["road"],
145 /// "Carrer de Calatrava"
146 /// );
147 ///```
148 pub fn reverse_full<T>(&self, point: &Point<T>) -> Result<OpencageResponse<T>, GeocodingError>
149 where
150 T: Float + DeserializeOwned + Debug,
151 {
152 let q = format!(
153 "{}, {}",
154 // OpenCage expects lat, lon order
155 (&point.y().to_f64().unwrap().to_string()),
156 &point.x().to_f64().unwrap().to_string()
157 );
158 let mut query = vec![
159 ("q", q.as_str()),
160 ("key", &self.api_key),
161 ("no_annotations", "0"),
162 ("no_record", "1"),
163 ];
164 query.extend(self.parameters.as_query());
165
166 let resp = self
167 .client
168 .get(&self.endpoint)
169 .query(&query)
170 .send()?
171 .error_for_status()?;
172 // it's OK to index into this vec, because reverse-geocoding only returns a single result
173 if let Some(headers) = resp.headers().get::<_>(XRL) {
174 let mut lock = self.remaining.try_lock();
175 if let Ok(ref mut mutex) = lock {
176 // not ideal, but typed headers are currently impossible in 0.9.x
177 let h = headers.to_str()?;
178 let h: i32 = h.parse()?;
179 **mutex = Some(h)
180 }
181 }
182 let res: OpencageResponse<T> = resp.json()?;
183 Ok(res)
184 }
185 /// A forward-geocoding lookup of an address, returning an annotated response.
186 ///
187 /// it is recommended that you restrict the search space by passing a
188 /// [bounding box](struct.InputBounds.html) to search within.
189 /// If you don't need or want to restrict the search using a bounding box (usually not recommended), you
190 /// may pass the [`NOBOX`](static.NOBOX.html) static value instead.
191 ///
192 /// Please see [the documentation](https://opencagedata.com/api#ambiguous-results) for details
193 /// of best practices in order to obtain good-quality results.
194 ///
195 /// This method passes the `no_record` parameter to the API.
196 ///
197 /// # Examples
198 ///
199 ///```
200 /// use geocoding::{Opencage, InputBounds, Point};
201 ///
202 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
203 /// let address = "UCL CASA";
204 /// // Optionally restrict the search space using a bounding box.
205 /// // The first point is the bottom-left corner, the second is the top-right.
206 /// let bbox = InputBounds::new(
207 /// Point::new(-0.13806939125061035, 51.51989264641164),
208 /// Point::new(-0.13427138328552246, 51.52319711775629),
209 /// );
210 /// let res = oc.forward_full(&address, bbox).unwrap();
211 /// let first_result = &res.results[0];
212 /// // the first result is correct
213 /// assert!(first_result.formatted.contains("UCL, 188 Tottenham Court Road"));
214 ///```
215 ///
216 /// ```
217 /// // You can pass NOBOX if you don't need bounds.
218 /// use geocoding::{Opencage, InputBounds, Point};
219 /// use geocoding::opencage::{NOBOX};
220 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
221 /// let address = "Moabit, Berlin";
222 /// let res = oc.forward_full(&address, NOBOX).unwrap();
223 /// let first_result = &res.results[0];
224 /// assert_eq!(
225 /// first_result.formatted,
226 /// "Moabit, Berlin, Germany"
227 /// );
228 /// ```
229 ///
230 /// ```
231 /// // There are several ways to construct a Point, such as from a tuple
232 /// use geocoding::{Opencage, InputBounds, Point};
233 /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
234 /// let address = "UCL CASA";
235 /// let bbox = InputBounds::new(
236 /// (-0.13806939125061035, 51.51989264641164),
237 /// (-0.13427138328552246, 51.52319711775629),
238 /// );
239 /// let res = oc.forward_full(&address, bbox).unwrap();
240 /// let first_result = &res.results[0];
241 /// assert!(
242 /// first_result.formatted.contains(
243 /// "UCL, 188 Tottenham Court Road"
244 /// ));
245 /// ```
246 pub fn forward_full<T, U>(
247 &self,
248 place: &str,
249 bounds: U,
250 ) -> Result<OpencageResponse<T>, GeocodingError>
251 where
252 T: Float + DeserializeOwned + Debug,
253 U: Into<Option<InputBounds<T>>>,
254 {
255 let ann = String::from("0");
256 let record = String::from("1");
257 // we need this to avoid lifetime inconvenience
258 let bd;
259 let mut query = vec![
260 ("q", place),
261 ("key", &self.api_key),
262 ("no_annotations", &ann),
263 ("no_record", &record),
264 ];
265
266 // If search bounds are passed, use them
267 if let Some(bds) = bounds.into() {
268 bd = String::from(bds);
269 query.push(("bounds", &bd));
270 }
271 query.extend(self.parameters.as_query());
272
273 let resp = self
274 .client
275 .get(&self.endpoint)
276 .query(&query)
277 .send()?
278 .error_for_status()?;
279 if let Some(headers) = resp.headers().get::<_>(XRL) {
280 let mut lock = self.remaining.try_lock();
281 if let Ok(ref mut mutex) = lock {
282 // not ideal, but typed headers are currently impossible in 0.9.x
283 let h = headers.to_str()?;
284 let h: i32 = h.parse()?;
285 **mutex = Some(h)
286 }
287 }
288 let res: OpencageResponse<T> = resp.json()?;
289 Ok(res)
290 }
291}
292
293impl<'a, T> Reverse<T> for Opencage<'a>
294where
295 T: Float + DeserializeOwned + Debug,
296{
297 /// A reverse lookup of a point. More detail on the format of the
298 /// returned `String` can be found [here](https://blog.opencagedata.com/post/99059889253/good-looking-addresses-solving-the-berlin-berlin)
299 ///
300 /// This method passes the `no_annotations` and `no_record` parameters to the API.
301 fn reverse(&self, point: &Point<T>) -> Result<Option<String>, GeocodingError> {
302 let q = format!(
303 "{}, {}",
304 // OpenCage expects lat, lon order
305 (&point.y().to_f64().unwrap().to_string()),
306 &point.x().to_f64().unwrap().to_string()
307 );
308 let mut query = vec![
309 ("q", q.as_str()),
310 ("key", &self.api_key),
311 ("no_annotations", "1"),
312 ("no_record", "1"),
313 ];
314 query.extend(self.parameters.as_query());
315
316 let resp = self
317 .client
318 .get(&self.endpoint)
319 .query(&query)
320 .send()?
321 .error_for_status()?;
322 if let Some(headers) = resp.headers().get::<_>(XRL) {
323 let mut lock = self.remaining.try_lock();
324 if let Ok(ref mut mutex) = lock {
325 // not ideal, but typed headers are currently impossible in 0.9.x
326 let h = headers.to_str()?;
327 let h: i32 = h.parse()?;
328 **mutex = Some(h)
329 }
330 }
331 let res: OpencageResponse<T> = resp.json()?;
332 // it's OK to index into this vec, because reverse-geocoding only returns a single result
333 let address = &res.results[0];
334 Ok(Some(address.formatted.to_string()))
335 }
336}
337
338impl<'a, T> Forward<T> for Opencage<'a>
339where
340 T: Float + DeserializeOwned + Debug,
341{
342 /// A forward-geocoding lookup of an address. Please see [the documentation](https://opencagedata.com/api#ambiguous-results) for details
343 /// of best practices in order to obtain good-quality results.
344 ///
345 /// This method passes the `no_annotations` and `no_record` parameters to the API.
346 fn forward(&self, place: &str) -> Result<Vec<Point<T>>, GeocodingError> {
347 let mut query = vec![
348 ("q", place),
349 ("key", &self.api_key),
350 ("no_annotations", "1"),
351 ("no_record", "1"),
352 ];
353 query.extend(self.parameters.as_query());
354
355 let resp = self
356 .client
357 .get(&self.endpoint)
358 .query(&query)
359 .send()?
360 .error_for_status()?;
361 if let Some(headers) = resp.headers().get::<_>(XRL) {
362 let mut lock = self.remaining.try_lock();
363 if let Ok(ref mut mutex) = lock {
364 // not ideal, but typed headers are currently impossible in 0.9.x
365 let h = headers.to_str()?;
366 let h: i32 = h.parse()?;
367 **mutex = Some(h)
368 }
369 }
370 let res: OpencageResponse<T> = resp.json()?;
371 Ok(res
372 .results
373 .iter()
374 .map(|res| Point::new(res.geometry["lng"], res.geometry["lat"]))
375 .collect())
376 }
377}
378
379/// The top-level full JSON response returned by a forward-geocoding request
380///
381/// See [the documentation](https://opencagedata.com/api#response) for more details
382///
383///```json
384/// {
385/// "documentation": "https://opencagedata.com/api",
386/// "licenses": [
387/// {
388/// "name": "CC-BY-SA",
389/// "url": "http://creativecommons.org/licenses/by-sa/3.0/"
390/// },
391/// {
392/// "name": "ODbL",
393/// "url": "http://opendatacommons.org/licenses/odbl/summary/"
394/// }
395/// ],
396/// "rate": {
397/// "limit": 2500,
398/// "remaining": 2499,
399/// "reset": 1523318400
400/// },
401/// "results": [
402/// {
403/// "annotations": {
404/// "DMS": {
405/// "lat": "41° 24' 5.06412'' N",
406/// "lng": "2° 7' 43.40064'' E"
407/// },
408/// "MGRS": "31TDF2717083684",
409/// "Maidenhead": "JN11bj56ki",
410/// "Mercator": {
411/// "x": 236968.295,
412/// "y": 5043465.71
413/// },
414/// "OSM": {
415/// "edit_url": "https://www.openstreetmap.org/edit?way=355421084#map=17/41.40141/2.12872",
416/// "url": "https://www.openstreetmap.org/?mlat=41.40141&mlon=2.12872#map=17/41.40141/2.12872"
417/// },
418/// "callingcode": 34,
419/// "currency": {
420/// "alternate_symbols": [
421///
422/// ],
423/// "decimal_mark": ",",
424/// "html_entity": "€",
425/// "iso_code": "EUR",
426/// "iso_numeric": 978,
427/// "name": "Euro",
428/// "smallest_denomination": 1,
429/// "subunit": "Cent",
430/// "subunit_to_unit": 100,
431/// "symbol": "€",
432/// "symbol_first": 1,
433/// "thousands_separator": "."
434/// },
435/// "flag": "🇪🇸",
436/// "geohash": "sp3e82yhdvd7p5x1mbdv",
437/// "qibla": 110.53,
438/// "sun": {
439/// "rise": {
440/// "apparent": 1523251260,
441/// "astronomical": 1523245440,
442/// "civil": 1523249580,
443/// "nautical": 1523247540
444/// },
445/// "set": {
446/// "apparent": 1523298360,
447/// "astronomical": 1523304180,
448/// "civil": 1523300040,
449/// "nautical": 1523302080
450/// }
451/// },
452/// "timezone": {
453/// "name": "Europe/Madrid",
454/// "now_in_dst": 1,
455/// "offset_sec": 7200,
456/// "offset_string": 200,
457/// "short_name": "CEST"
458/// },
459/// "what3words": {
460/// "words": "chins.pictures.passes"
461/// }
462/// },
463/// "bounds": {
464/// "northeast": {
465/// "lat": 41.4015815,
466/// "lng": 2.128952
467/// },
468/// "southwest": {
469/// "lat": 41.401227,
470/// "lng": 2.1284918
471/// }
472/// },
473/// "components": {
474/// "ISO_3166-1_alpha-2": "ES",
475/// "_type": "building",
476/// "city": "Barcelona",
477/// "city_district": "Sarrià - Sant Gervasi",
478/// "country": "Spain",
479/// "country_code": "es",
480/// "county": "BCN",
481/// "house_number": "68",
482/// "political_union": "European Union",
483/// "postcode": "08017",
484/// "road": "Carrer de Calatrava",
485/// "state": "Catalonia",
486/// "suburb": "les Tres Torres"
487/// },
488/// "confidence": 10,
489/// "formatted": "Carrer de Calatrava, 68, 08017 Barcelona, Spain",
490/// "geometry": {
491/// "lat": 41.4014067,
492/// "lng": 2.1287224
493/// }
494/// }
495/// ],
496/// "status": {
497/// "code": 200,
498/// "message": "OK"
499/// },
500/// "stay_informed": {
501/// "blog": "https://blog.opencagedata.com",
502/// "twitter": "https://twitter.com/opencagedata"
503/// },
504/// "thanks": "For using an OpenCage Data API",
505/// "timestamp": {
506/// "created_http": "Mon, 09 Apr 2018 12:33:01 GMT",
507/// "created_unix": 1523277181
508/// },
509/// "total_results": 1
510/// }
511///```
512#[derive(Debug, Serialize, Deserialize)]
513pub struct OpencageResponse<T>
514where
515 T: Float,
516{
517 pub documentation: String,
518 pub licenses: Vec<HashMap<String, String>>,
519 pub rate: Option<HashMap<String, i32>>,
520 pub results: Vec<Results<T>>,
521 pub status: Status,
522 pub stay_informed: HashMap<String, String>,
523 pub thanks: String,
524 pub timestamp: Timestamp,
525 pub total_results: i32,
526}
527
528/// A forward geocoding result
529#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct Results<T>
531where
532 T: Float,
533{
534 pub annotations: Option<Annotations<T>>,
535 pub bounds: Option<Bounds<T>>,
536 pub components: HashMap<String, serde_json::Value>,
537 pub confidence: i8,
538 pub formatted: String,
539 pub geometry: HashMap<String, T>,
540}
541
542/// Annotations pertaining to the geocoding result
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct Annotations<T>
545where
546 T: Float,
547{
548 pub dms: Option<HashMap<String, String>>,
549 pub mgrs: Option<String>,
550 pub maidenhead: Option<String>,
551 pub mercator: Option<HashMap<String, T>>,
552 pub osm: Option<HashMap<String, String>>,
553 pub callingcode: i16,
554 pub currency: Option<Currency>,
555 pub flag: String,
556 pub geohash: String,
557 pub qibla: T,
558 pub sun: Sun,
559 pub timezone: Timezone,
560 pub what3words: HashMap<String, String>,
561}
562
563/// Currency metadata
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct Currency {
566 pub alternate_symbols: Option<Vec<String>>,
567 pub decimal_mark: String,
568 pub html_entity: String,
569 pub iso_code: String,
570 #[serde(deserialize_with = "deserialize_string_or_int")]
571 pub iso_numeric: String,
572 pub name: String,
573 pub smallest_denomination: i16,
574 pub subunit: String,
575 pub subunit_to_unit: i16,
576 pub symbol: String,
577 pub symbol_first: i16,
578 pub thousands_separator: String,
579}
580
581/// Sunrise and sunset metadata
582#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct Sun {
584 pub rise: HashMap<String, i64>,
585 pub set: HashMap<String, i64>,
586}
587
588/// Timezone metadata
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct Timezone {
591 pub name: String,
592 pub now_in_dst: i16,
593 pub offset_sec: i32,
594 #[serde(deserialize_with = "deserialize_string_or_int")]
595 pub offset_string: String,
596 #[serde(deserialize_with = "deserialize_string_or_int")]
597 pub short_name: String,
598}
599
600/// HTTP status metadata
601#[derive(Debug, Serialize, Deserialize)]
602pub struct Status {
603 pub message: String,
604 pub code: i16,
605}
606
607/// Timestamp metadata
608#[derive(Debug, Serialize, Deserialize)]
609pub struct Timestamp {
610 pub created_http: String,
611 #[serde(deserialize_with = "from_ts")]
612 pub created_unix: NaiveDateTime,
613}
614
615/// Bounding-box metadata
616#[derive(Debug, Clone, Serialize, Deserialize)]
617pub struct Bounds<T>
618where
619 T: Float,
620{
621 pub northeast: HashMap<String, T>,
622 pub southwest: HashMap<String, T>,
623}
624
625#[cfg(test)]
626mod test {
627 use super::*;
628 use crate::Coordinate;
629
630 #[test]
631 fn reverse_test() {
632 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
633 let p = Point::new(2.12870, 41.40139);
634 let res = oc.reverse(&p);
635 assert_eq!(
636 res.unwrap(),
637 Some("Carrer de Calatrava, 68, 08017 Barcelona, Spain".to_string())
638 );
639 }
640
641 #[test]
642 fn reverse_test_with_params() {
643 let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
644 oc.parameters.language = Some("fr");
645 let p = Point::new(2.12870, 41.40139);
646 let res = oc.reverse(&p);
647 assert_eq!(
648 res.unwrap(),
649 Some("Carrer de Calatrava, 68, 08017 Barcelone, Espagne".to_string())
650 );
651 }
652 #[test]
653 fn forward_test() {
654 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
655 let address = "Schwabing, München";
656 let res = oc.forward(&address);
657 assert_eq!(
658 res.unwrap(),
659 vec![Point(Coordinate {
660 x: 11.5884858,
661 y: 48.1700887
662 })]
663 );
664 }
665 #[test]
666 fn reverse_full_test() {
667 let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
668 oc.parameters.language = Some("fr");
669 let p = Point::new(2.12870, 41.40139);
670 let res = oc.reverse_full(&p).unwrap();
671 let first_result = &res.results[0];
672 assert_eq!(first_result.components["road"], "Carrer de Calatrava");
673 }
674 #[test]
675 fn forward_full_test() {
676 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
677 let address = "UCL CASA";
678 let bbox = InputBounds {
679 minimum_lonlat: Point::new(-0.13806939125061035, 51.51989264641164),
680 maximum_lonlat: Point::new(-0.13427138328552246, 51.52319711775629),
681 };
682 let res = oc.forward_full(&address, bbox).unwrap();
683 let first_result = &res.results[0];
684 assert!(first_result.formatted.contains("UCL"));
685 }
686 #[test]
687 fn forward_full_test_floats() {
688 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
689 let address = "UCL CASA";
690 let bbox = InputBounds::new(
691 Point::new(-0.13806939125061035, 51.51989264641164),
692 Point::new(-0.13427138328552246, 51.52319711775629),
693 );
694 let res = oc.forward_full(&address, bbox).unwrap();
695 let first_result = &res.results[0];
696 assert!(first_result
697 .formatted
698 .contains("UCL, 188 Tottenham Court Road"));
699 }
700 #[test]
701 fn forward_full_test_pointfrom() {
702 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
703 let address = "UCL CASA";
704 let bbox = InputBounds::new(
705 Point::from((-0.13806939125061035, 51.51989264641164)),
706 Point::from((-0.13427138328552246, 51.52319711775629)),
707 );
708 let res = oc.forward_full(&address, bbox).unwrap();
709 let first_result = &res.results[0];
710 assert!(first_result
711 .formatted
712 .contains("UCL, 188 Tottenham Court Road"));
713 }
714 #[test]
715 fn forward_full_test_pointinto() {
716 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
717 let address = "UCL CASA";
718 let bbox = InputBounds::new(
719 (-0.13806939125061035, 51.51989264641164),
720 (-0.13427138328552246, 51.52319711775629),
721 );
722 let res = oc.forward_full(&address, bbox).unwrap();
723 let first_result = &res.results[0];
724 assert!(first_result
725 .formatted
726 .contains("Tottenham Court Road, London"));
727 }
728 #[test]
729 fn forward_full_test_nobox() {
730 let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
731 let address = "Moabit, Berlin, Germany";
732 let res = oc.forward_full(&address, NOBOX).unwrap();
733 let first_result = &res.results[0];
734 assert_eq!(first_result.formatted, "Moabit, Berlin, Germany");
735 }
736}