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": "&#x20AC;",
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}