New Upstream Snapshot - ruby-geocoder

Ready changes

Summary

Merged new upstream version: 1.8.1+git20230105.1.6f2f423 (was: 1.5.1).

Resulting package

Built on 2023-01-11T04:31 (took 25m19s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-geocoder

Diff

diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..170ccb6
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,39 @@
+language: ruby
+cache: bundler
+
+services:
+  - postgresql
+  - mysql
+before_script:
+  - psql -c 'create database geocoder_test;' -U postgres
+
+before_install:
+  - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
+  - gem install bundler -v '< 2'
+
+env:
+  global: 
+    - JRUBY_OPTS=--2.0
+  matrix:
+    - DB=
+    - DB=sqlite
+    - DB=sqlite USE_SQLITE_EXT=1
+    - DB=postgres
+    - DB=mysql
+rvm:
+  - 2.5.9
+  - 2.6.8
+  - 2.7.4
+  - 3.0.2
+  - jruby-19mode
+gemfile:
+  - Gemfile
+  - gemfiles/Gemfile.rails5.0
+matrix:
+  exclude:
+    - env: DB=
+      gemfile: gemfiles/Gemfile.rails5.0
+    - rvm: jruby-19mode
+      gemfile: gemfiles/Gemfile.rails5.0
+    - env: DB=sqlite USE_SQLITE_EXT=1
+      rvm: jruby-19mode
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cf518a..4aaaddf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,93 @@ Changelog
 
 Major changes to Geocoder for each release. Please see the Git log for complete list of changes.
 
+1.8.1 (2022 Sep 23)
+-------------------
+* Add support for IPBase lookup (thanks github.com/jonallured).
+* Test cleanup (thanks github.com/jonallured).
+* Prevent errors when existing constant name shadows a lookup class (thanks github.com/avram-twitch).
+
+1.8.0 (2022 May 17)
+-------------------
+* Add support for 2GIS lookup (thanks github.com/ggrikgg).
+* Change cache configuration structure and add an expiration option. Cache prefix is now set via {cache_options: {prefix: ...}} instead of {cache_prefix: ...}. See README for details.
+* Add `:fields` parameter for :google_places_details and :google_places_search lookups. If you haven't been requesting specific fields, you may start getting different data (defaults are now the APIs' defaults). See for details: https://github.com/alexreisner/geocoder/pull/1572 (thanks github.com/czlee).
+* Update :here lookup to use API version 7. Query options are different, API key must be a string (not an array). See API docs at https://developer.here.com/documentation/geocoding-search-api/api-reference-swagger.html (thanks github.com/Pritilender).
+
+1.7.5 (2022 Mar 14)
+-------------------
+* Avoid lookup naming collisions in some environments.
+
+1.7.4 (2022 Mar 14)
+-------------------
+* Add ability to use app-defined lookups (thanks github.com/januszm).
+* Updates to LocationIQ and FreeGeoIP lookups.
+
+1.7.3 (2022 Jan 17)
+-------------------
+* Get rid of unnecessary cache_prefix deprecation warnings.
+
+1.7.2 (2022 Jan  2)
+-------------------
+* Fix uninitialized constant error (occurring on some systems with v1.7.1).
+
+1.7.1 (2022 Jan  1)
+-------------------
+* Various bugfixes and refactorings.
+
+1.7.0 (2021 Oct 11)
+-------------------
+* Add support for Geoapify and Photo lookups (thanks github.com/ahukkanen).
+* Add support for IPQualityScore IP lookup (thanks github.com/jamesbebbington).
+* Add support for Amazon Location Service lookup (thanks github.com/mplewis).
+* Add support for Melissa lookup (thanks github.com/ALacker).
+* Drop official support for Ruby 2.0.x and Rails 4.x.
+
+1.6.7 (2021 Apr 17)
+-------------------
+* Add support for Abstract API lookup (thanks github.com/randoum).
+
+1.6.6 (2021 Mar  4)
+-------------------
+* Rescue from exception on cache read/write error. Issue warning instead.
+
+1.6.5 (2021 Feb 10)
+-------------------
+* Fix backward coordinates bug in NationaalregisterNl lookup (thanks github.com/Marthyn).
+* Allow removal of single stubs in test mode (thanks github.com/jmmastey).
+* Improve results for :ban_data_gouv_fr lookup (thanks github.com/Intrepidd).
+
+1.6.4 (2020 Oct  6)
+-------------------
+* Various updates in response to geocoding API changes.
+* Refactor of Google Places Search lookup (thanks github.com/maximilientyc).
+
+1.6.3 (2020 Apr 30)
+-------------------
+* Update URL for :telize lookup (thanks github.com/alexwalling).
+* Fix bug parsing IPv6 with port (thanks github.com/gdomingu).
+
+1.6.2 (2020 Mar 16)
+-------------------
+* Add support for :nationaal_georegister_nl lookup (thanks github.com/opensourceame).
+* Add support for :uk_ordnance_survey_names lookup (thanks github.com/pezholio).
+* Refactor and fix bugs in Yandex lookup (thanks github.com/iarie and stereodenis).
+
+1.6.1 (2020 Jan 23)
+-------------------
+* Sanitize lat/lon values passed to within_bounding_box to prevent SQL injection.
+
+1.6.0 (2020 Jan  6)
+-------------------
+* Drop support for Rails 3.x.
+* Add support for :osmnames lookup (thanks github.com/zacviandier).
+* Add support for :ipgeolocation IP lookup (thanks github.com/ahsannawaz111).
+
+1.5.2 (2019 Oct  3)
+-------------------
+* Add support for :ipregistry lookup (thanks github.com/ipregistry).
+* Various fixes for Yandex lookup.
+
 1.5.1 (2019 Jan 23)
 -------------------
 * Add support for :tencent lookup (thanks github.com/Anders-E).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..4e8f6fb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+Contributing
+------------
+
+Reporting Bugs
+==============
+
+When reporting an issue, please list the version of Geocoder you are using and any relevant information about your application (Rails version, database type and version, etc). Please describe as specifically as you can what behavior you are seeing (eg: an error message? a nil return value?).
+
+Please DO NOT use GitHub issues to ask questions about how to use Geocoder. Sites like [StackOverflow](http://www.stackoverflow.com/) are a better forum for such discussions.
+
+
+Making Changes
+==============
+
+Changes are welcome via Github pull requests. If you are new to the project and looking for a way to get involved, try picking up an issue with a "beginner-task" label. Hints about what needs to be done are usually provided.
+
+For all contributions, please respect the following guidelines:
+
+* Each pull request should implement ONE feature or bugfix. If you want to add or fix more than one thing, submit more than one pull request.
+* Do not commit changes to files that are irrelevant to your feature or bugfix (eg: `.gitignore`).
+* Do not add dependencies on other gems.
+* Do not add unnecessary `require` statements which could cause LoadErrors on certain systems.
+* Remember: Geocoder needs to run outside of Rails. Don't assume things like ActiveSupport are available.
+* Be willing to accept criticism and work on improving your code; Geocoder is used by thousands of developers and care must be taken not to introduce bugs.
+* Be aware that the pull request review process is not immediate, and is generally proportional to the size of the pull request.
+* If your pull request is merged, please do not ask for an immediate release of the gem. There are many factors contributing to when releases occur (remember that they affect thousands of apps with Geocoder in their Gemfiles). If necessary, please install from the Github source until the next official release.
+
+
+Copyright :copyright: 2009-2021 Alex Reisner, released under the MIT license.
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..5124996
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,42 @@
+source "https://rubygems.org"
+
+group :development, :test do
+  gem 'rake'
+  gem 'mongoid'
+  gem 'geoip'
+  gem 'rubyzip'
+  gem 'rails', '~>5.1.0'
+  gem 'test-unit' # needed for Ruby >=2.2.0
+
+  platforms :jruby do
+    gem 'jruby-openssl'
+    gem 'jgeoip'
+  end
+
+  platforms :rbx do
+    gem 'rubysl', '~> 2.0'
+    gem 'rubysl-test-unit'
+  end
+end
+
+group :test do
+  platforms :ruby, :mswin, :mingw do
+    gem 'sqlite3', '~> 1.4.2'
+    gem 'sqlite_ext', '~> 1.5.0'
+  end
+
+  gem 'webmock'
+
+  platforms :ruby do
+    gem 'pg', '~> 0.11'
+    gem 'mysql2', '~> 0.5.4'
+  end
+
+  platforms :jruby do
+    gem 'jdbc-mysql'
+    gem 'jdbc-sqlite3'
+    gem 'activerecord-jdbcpostgresql-adapter'
+  end
+end
+
+gemspec
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..59fa0ff
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,20 @@
+BEFORE POSTING AN ISSUE, PLEASE MAKE SURE THE PROBLEM IS NOT ADDRESSED IN THE README!
+
+### Expected behavior
+
+
+
+### Actual behavior
+
+
+
+### Steps to reproduce
+
+
+
+### Environment info
+
+* Geocoder version: 
+* Rails version: 
+* Database (if applicable): 
+* Lookup (if applicable): 
diff --git a/LICENSE b/LICENSE
index 767580c..523692c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009-11 Alex Reisner
+Copyright (c) 2009-2021 Alex Reisner
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
diff --git a/README.md b/README.md
index 4e2c258..f1719b9 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,27 @@
 Geocoder
 ========
 
-**A complete geocoding solution for Ruby.**
+**Complete geocoding solution for Ruby.**
 
 [![Gem Version](https://badge.fury.io/rb/geocoder.svg)](http://badge.fury.io/rb/geocoder)
 [![Code Climate](https://codeclimate.com/github/alexreisner/geocoder/badges/gpa.svg)](https://codeclimate.com/github/alexreisner/geocoder)
-[![Build Status](https://travis-ci.org/alexreisner/geocoder.svg?branch=master)](https://travis-ci.org/alexreisner/geocoder)
-[![GitHub Issues](https://img.shields.io/github/issues/alexreisner/geocoder.svg)](https://github.com/alexreisner/geocoder/issues)
-[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![Build Status](https://travis-ci.com/alexreisner/geocoder.svg?branch=master)](https://travis-ci.com/alexreisner/geocoder)
 
 Key features:
 
-* Forward and reverse geocoding, and IP address geocoding.
+* Forward and reverse geocoding.
+* IP address geocoding.
 * Connects to more than 40 APIs worldwide.
 * Performance-enhancing features like caching.
-* Advanced configuration allows different parameters and APIs to be used in different conditions.
 * Integrates with ActiveRecord and Mongoid.
 * Basic geospatial queries: search within radius (or rectangle, or ring).
 
 Compatibility:
 
-* Supports multiple Ruby versions: Ruby 2.x, and JRuby.
-* Supports multiple databases: MySQL, PostgreSQL, SQLite, and MongoDB (1.7.0 and higher).
-* Supports Rails 3, 4, and 5. If you need to use it with Rails 2 please see the `rails2` branch (no longer maintained, limited feature set).
-* Works very well outside of Rails, you just need to install either the `json` (for MRI) or `json_pure` (for JRuby) gem.
+* Ruby versions: 2.1+, and JRuby.
+* Databases: MySQL, PostgreSQL, SQLite, and MongoDB.
+* Rails: 5.x, 6.x, and 7.x.
+* Works outside of Rails with the `json` (for MRI) or `json_pure` (for JRuby) gem.
 
 
 Table of Contents
@@ -53,8 +51,8 @@ The Rest:
 * [Technical Discussions](#technical-discussions)
 * [Troubleshooting](#troubleshooting)
 * [Known Issues](#known-issues)
-* [Reporting Issues](#reporting-issues)
-* [Contributing](#contributing)
+* [Reporting Issues](https://github.com/alexreisner/geocoder/blob/master/CONTRIBUTING.md#reporting-bugs)
+* [Contributing](https://github.com/alexreisner/geocoder/blob/master/CONTRIBUTING.md#making-changes)
 
 See Also:
 
@@ -66,23 +64,29 @@ Basic Search
 
 In its simplest form, Geocoder takes an address and searches for its latitude/longitude coordinates:
 
-    results = Geocoder.search("Paris")
-    results.first.coordinates
-    => [48.856614, 2.3522219]  # latitude and longitude
+```ruby
+results = Geocoder.search("Paris")
+results.first.coordinates
+# => [48.856614, 2.3522219]  # latitude and longitude
+```
 
 The reverse is possible too. Given coordinates, it finds an address:
 
-    results = Geocoder.search([48.856614, 2.3522219])
-    results.first.address
-    => "Hôtel de Ville, 75004 Paris, France"
+```ruby
+results = Geocoder.search([48.856614, 2.3522219])
+results.first.address
+# => "Hôtel de Ville, 75004 Paris, France"
+```
 
-You can also look up the location of an IP addresses:
+You can also look up the location of an IP address:
 
-    results = Geocoder.search("172.56.21.89")
-    results.first.coordinates
-    => [30.267153, -97.7430608]
-    results.first.country
-    => "United States"
+```ruby
+results = Geocoder.search("172.56.21.89")
+results.first.coordinates
+# => [30.267153, -97.7430608]
+results.first.country
+# => "United States"
+```
 
 **The success and accuracy of geocoding depends entirely on the API being used to do these lookups.** Most queries work fairly well with the default configuration, but every application has different needs and every API has its particular strengths and weaknesses. If you need better coverage for your application you'll want to get familiar with the large number of supported APIs, listed in the [API Guide](https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md).
 
@@ -94,30 +98,40 @@ To automatically geocode your objects:
 
 **1.** Your model must provide a method that returns an address to geocode. This can be a single attribute, but it can also be a method that returns a string assembled from different attributes (eg: `city`, `state`, and `country`). For example, if your model has `street`, `city`, `state`, and `country` attributes you might do something like this:
 
-    def address
-      [street, city, state, country].compact.join(', ')
-    end
+```ruby
+def address
+  [street, city, state, country].compact.join(', ')
+end
+```
 
 **2.** Your model must have a way to store latitude/longitude coordinates. With ActiveRecord, add two attributes/columns (of type float or decimal) called `latitude` and `longitude`. For MongoDB, use a single field (of type Array) called `coordinates` (i.e., `field :coordinates, type: Array`). (See [Advanced Model Configuration](#advanced-model-configuration) for using different attribute names.)
 
 **3.** In your model, tell geocoder where to find the object's address:
 
-    geocoded_by :address
+```ruby
+geocoded_by :address
+```
 
 This adds a `geocode` method which you can invoke via callback:
 
-    after_validation :geocode
+```ruby
+after_validation :geocode
+```
 
 Reverse geocoding (given lat/lon coordinates, find an address) is similar:
 
-    reverse_geocoded_by :latitude, :longitude
-    after_validation :reverse_geocode
+```ruby
+reverse_geocoded_by :latitude, :longitude
+after_validation :reverse_geocode
+```
 
 With any geocoded objects, you can do the following:
 
-    obj.distance_to([43.9,-98.6])  # distance from obj to point
-    obj.bearing_to([43.9,-98.6])   # bearing from obj to point
-    obj.bearing_from(obj2)         # bearing from obj2 to obj
+```ruby
+obj.distance_to([43.9,-98.6])  # distance from obj to point
+obj.bearing_to([43.9,-98.6])   # bearing from obj to point
+obj.bearing_from(obj2)         # bearing from obj2 to obj
+```
 
 The `bearing_from/to` methods take a single argument which can be: a `[lat,lon]` array, a geocoded object, or a geocodable address (string). The `distance_from/to` methods also take a units argument (`:mi`, `:km`, or `:nm` for nautical miles). See [Distance and Bearing](#distance-and-bearing) below for more info.
 
@@ -125,18 +139,24 @@ The `bearing_from/to` methods take a single argument which can be: a `[lat,lon]`
 
 Before you can call `geocoded_by` you'll need to include the necessary module using one of the following:
 
-    include Geocoder::Model::Mongoid
-    include Geocoder::Model::MongoMapper
+```ruby
+include Geocoder::Model::Mongoid
+include Geocoder::Model::MongoMapper
+```
 
 ### Latitude/Longitude Order in MongoDB
 
 Everywhere coordinates are passed to methods as two-element arrays, Geocoder expects them to be in the order: `[lat, lon]`. However, as per [the GeoJSON spec](http://geojson.org/geojson-spec.html#positions), MongoDB requires that coordinates be stored longitude-first (`[lon, lat]`), so internally they are stored "backwards." Geocoder's methods attempt to hide this, so calling `obj.to_coordinates` (a method added to the object by Geocoder via `geocoded_by`) returns coordinates in the conventional order:
 
-    obj.to_coordinates  # => [37.7941013, -122.3951096] # [lat, lon]
+```ruby
+obj.to_coordinates  # => [37.7941013, -122.3951096] # [lat, lon]
+```
 
 whereas calling the object's coordinates attribute directly (`obj.coordinates` by default) returns the internal representation which is probably the reverse of what you want:
 
-    obj.coordinates     # => [-122.3951096, 37.7941013] # [lon, lat]
+```ruby
+obj.coordinates     # => [-122.3951096, 37.7941013] # [lon, lat]
+```
 
 So, be careful.
 
@@ -144,7 +164,9 @@ So, be careful.
 
 To use Geocoder with ActiveRecord and a framework other than Rails (like Sinatra or Padrino), you will need to add this in your model before calling Geocoder methods:
 
-    extend Geocoder::Model::ActiveRecord
+```ruby
+extend Geocoder::Model::ActiveRecord
+```
 
 
 Geospatial Database Queries
@@ -154,19 +176,23 @@ Geospatial Database Queries
 
 To find objects by location, use the following scopes:
 
-    Venue.near('Omaha, NE, US')                   # venues within 20 miles of Omaha
-    Venue.near([40.71, -100.23], 50)              # venues within 50 miles of a point
-    Venue.near([40.71, -100.23], 50, units: :km)  # venues within 50 kilometres of a point
-    Venue.geocoded                                # venues with coordinates
-    Venue.not_geocoded                            # venues without coordinates
+```ruby
+Venue.near('Omaha, NE, US')                   # venues within 20 miles of Omaha
+Venue.near([40.71, -100.23], 50)              # venues within 50 miles of a point
+Venue.near([40.71, -100.23], 50, units: :km)  # venues within 50 kilometres of a point
+Venue.geocoded                                # venues with coordinates
+Venue.not_geocoded                            # venues without coordinates
+```
 
 With geocoded objects you can do things like this:
 
-    if obj.geocoded?
-      obj.nearbys(30)                       # other objects within 30 miles
-      obj.distance_from([40.714,-100.234])  # distance from arbitrary point to object
-      obj.bearing_to("Paris, France")       # direction from object to arbitrary point
-    end
+```ruby
+if obj.geocoded?
+  obj.nearbys(30)                       # other objects within 30 miles
+  obj.distance_from([40.714,-100.234])  # distance from arbitrary point to object
+  obj.bearing_to("Paris, France")       # direction from object to arbitrary point
+end
+```
 
 ### For MongoDB-backed models:
 
@@ -178,8 +204,10 @@ Geocoding HTTP Requests
 
 Geocoder adds `location` and `safe_location` methods to the standard `Rack::Request` object so you can easily look up the location of any HTTP request by IP address. For example, in a Rails controller or a Sinatra app:
 
-    # returns Geocoder::Result object
-    result = request.location
+```ruby
+# returns Geocoder::Result object
+result = request.location
+```
 
 **The `location` method is vulnerable to trivial IP address spoofing via HTTP headers.**  If that's a problem for your application, use `safe_location` instead, but be aware that `safe_location` will *not* try to trace a request's originating IP through proxy headers; you will instead get the location of the last proxy the request passed through, if any (excepting any proxies you have explicitly whitelisted in your Rack config).
 
@@ -193,71 +221,82 @@ Geocoder supports a variety of street and IP address geocoding services. The def
 
 To create a Rails initializer with sample configuration:
 
-    rails generate geocoder:config
+```sh
+rails generate geocoder:config
+```
 
 Some common options are:
 
-    # config/initializers/geocoder.rb
-    Geocoder.configure(
+```ruby
+# config/initializers/geocoder.rb
+Geocoder.configure(
+  # street address geocoding service (default :nominatim)
+  lookup: :yandex,
 
-      # street address geocoding service (default :nominatim)
-      lookup: :yandex,
+  # IP address geocoding service (default :ipinfo_io)
+  ip_lookup: :maxmind,
 
-      # IP address geocoding service (default :ipinfo_io)
-      ip_lookup: :maxmind,
+  # to use an API key:
+  api_key: "...",
 
-      # to use an API key:
-      api_key: "...",
+  # geocoding service request timeout, in seconds (default 3):
+  timeout: 5,
 
-      # geocoding service request timeout, in seconds (default 3):
-      timeout: 5,
+  # set default units to kilometers:
+  units: :km,
 
-      # set default units to kilometers:
-      units: :km,
-
-      # caching (see [below](#caching) for details):
-      cache: Redis.new,
-      cache_prefix: "..."
-
-    )
+  # caching (see Caching section below for details):
+  cache: Redis.new,
+  cache_options: {
+    expiration: 1.day, # Defaults to `nil`
+    prefix: "another_key:" # Defaults to `geocoder:`
+  }
+)
+```
 
 Please see [`lib/geocoder/configuration.rb`](https://github.com/alexreisner/geocoder/blob/master/lib/geocoder/configuration.rb) for a complete list of configuration options. Additionally, some lookups have their own special configuration options which are directly supported by Geocoder. For example, to specify a value for Google's `bounds` parameter:
 
-    # with Google:
-    Geocoder.search("Middletown", bounds: [[40.6,-77.9], [39.9,-75.9]])
+```ruby
+# with Google:
+Geocoder.search("Middletown", bounds: [[40.6,-77.9], [39.9,-75.9]])
+```
 
 Please see the [source code for each lookup](https://github.com/alexreisner/geocoder/tree/master/lib/geocoder/lookups) to learn about directly supported parameters. Parameters which are not directly supported can be specified using the `:params` option, which appends options to the query string of the geocoding request. For example:
 
-    # Nominatim's `countrycodes` parameter:
-    Geocoder.search("Rome", params: {countrycodes: "us,ca"})
+```ruby
+# Nominatim's `countrycodes` parameter:
+Geocoder.search("Rome", params: {countrycodes: "us,ca"})
 
-    # Google's `region` parameter:
-    Geocoder.search("Rome", params: {region: "..."})
+# Google's `region` parameter:
+Geocoder.search("Rome", params: {region: "..."})
+```
 
 ### Configuring Multiple Services
 
 You can configure multiple geocoding services at once by using the service's name as a key for a sub-configuration hash, like this:
 
-    Geocoder.configure(
+```ruby
+Geocoder.configure(
 
-      timeout: 2,
-      cache: Redis.new,
+  timeout: 2,
+  cache: Redis.new,
 
-      yandex: {
-        api_key: "...",
-        timeout: 5
-      },
+  yandex: {
+    api_key: "...",
+    timeout: 5
+  },
 
-      baidu: {
-        api_key: "..."
-      },
+  baidu: {
+    api_key: "..."
+  },
 
-      maxmind: {
-        api_key: "...",
-        service: :omni
-      }
+  maxmind: {
+    api_key: "...",
+    service: :omni
+  }
 
-    )
+)
+```
 
 Lookup-specific settings override global settings so, in this example, the timeout for all lookups is 2 seconds, except for Yandex which is 5.
 
@@ -269,12 +308,16 @@ Performance and Optimization
 
 In MySQL and Postgres, queries use a bounding box to limit the number of points over which a more precise distance calculation needs to be done. To take advantage of this optimisation, you need to add a composite index on latitude and longitude. In your Rails migration:
 
-    add_index :table, [:latitude, :longitude]
+```ruby
+add_index :table, [:latitude, :longitude]
+```
 
 In MongoDB, by default, the methods `geocoded_by` and `reverse_geocoded_by` create a geospatial index. You can avoid index creation with the `:skip_index option`, for example:
 
-    include Geocoder::Model::Mongoid
-    geocoded_by :address, skip_index: true
+```ruby
+include Geocoder::Model::Mongoid
+geocoded_by :address, skip_index: true
+```
 
 ### Avoiding Unnecessary API Requests
 
@@ -285,13 +328,17 @@ Geocoding only needs to be performed under certain conditions. To avoid unnecess
 
 The exact code will vary depending on the method you use for your geocodable string, but it would be something like this:
 
-    after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? }
+```ruby
+after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? }
+```
 
 ### Caching
 
 When relying on any external service, it's always a good idea to cache retrieved data. When implemented correctly, it improves your app's response time and stability. It's easy to cache geocoding results with Geocoder -- just configure a cache store:
 
-    Geocoder.configure(cache: Redis.new)
+```ruby
+Geocoder.configure(cache: Redis.new)
+```
 
 This example uses Redis, but the cache store can be any object that supports these methods:
 
@@ -302,20 +349,30 @@ This example uses Redis, but the cache store can be any object that supports the
 
 Even a plain Ruby hash will work, though it's not a great choice (cleared out when app is restarted, not shared between app instances, etc).
 
+When using Rails use the Generic cache store as an adapter around `Rails.cache`:
+
+```ruby
+Geocoder.configure(cache: Geocoder::CacheStore::Generic.new(Rails.cache, {}))
+```
+
 You can also set a custom prefix to be used for cache keys:
 
-    Geocoder.configure(cache_prefix: "...")
+```ruby
+Geocoder.configure(cache_options: { prefix: "..." })
+```
 
 By default the prefix is `geocoder:`
 
 If you need to expire cached content:
 
-    Geocoder::Lookup.get(Geocoder.config[:lookup]).cache.expire(:all)  # expire cached results for current Lookup
-    Geocoder::Lookup.get(:nominatim).cache.expire("http://...")        # expire cached result for a specific URL
-    Geocoder::Lookup.get(:nominatim).cache.expire(:all)                # expire cached results for Google Lookup
-    # expire all cached results for all Lookups.
-    # Be aware that this methods spawns a new Lookup object for each Service
-    Geocoder::Lookup.all_services.each{|service| Geocoder::Lookup.get(service).cache.expire(:all)}
+```ruby
+Geocoder::Lookup.get(Geocoder.config[:lookup]).cache.expire(:all)  # expire cached results for current Lookup
+Geocoder::Lookup.get(:nominatim).cache.expire("http://...")        # expire cached result for a specific URL
+Geocoder::Lookup.get(:nominatim).cache.expire(:all)                # expire cached results for Google Lookup
+# expire all cached results for all Lookups.
+# Be aware that this methods spawns a new Lookup object for each Service
+Geocoder::Lookup.all_services.each{|service| Geocoder::Lookup.get(service).cache.expire(:all)}
+```
 
 Do *not* include the prefix when passing a URL to be expired. Expiring `:all` will only expire keys with the configured prefix -- it will *not* expire every entry in your key/value store.
 
@@ -329,44 +386,55 @@ Advanced Model Configuration
 
 You are not stuck with the `latitude` and `longitude` database column names (with ActiveRecord) or the `coordinates` array (Mongo) for storing coordinates. For example:
 
-    geocoded_by :address, latitude: :lat, longitude: :lon  # ActiveRecord
-    geocoded_by :address, coordinates: :coords             # MongoDB
+```ruby
+geocoded_by :address, latitude: :lat, longitude: :lon  # ActiveRecord
+geocoded_by :address, coordinates: :coords             # MongoDB
+```
 
 For reverse geocoding, you can specify the attribute where the address will be stored. For example:
 
-    reverse_geocoded_by :latitude, :longitude, address: :loc    # ActiveRecord
-    reverse_geocoded_by :coordinates, address: :street_address  # MongoDB
+```ruby
+reverse_geocoded_by :latitude, :longitude, address: :loc    # ActiveRecord
+reverse_geocoded_by :coordinates, address: :street_address  # MongoDB
+```
 
 To specify geocoding parameters in your model:
 
-    geocoded_by :address, params: {region: "..."}
+```ruby
+geocoded_by :address, params: {region: "..."}
+```
 
 Supported parameters: `:lookup`, `:ip_lookup`, `:language`, and `:params`. You can specify an anonymous function if you want to set these on a per-request basis. For example, to use different lookups for objects in different regions:
 
-    geocoded_by :address, lookup: lambda{ |obj| obj.geocoder_lookup }
+```ruby
+geocoded_by :address, lookup: lambda{ |obj| obj.geocoder_lookup }
 
-    def geocoder_lookup
-      if country_code == "RU"
-        :yandex
-      elsif country_code == "CN"
-        :baidu
-      else
-        :nominatim
-      end
-    end
+def geocoder_lookup
+  if country_code == "RU"
+    :yandex
+  elsif country_code == "CN"
+    :baidu
+  else
+    :nominatim
+  end
+end
+```
 
 ### Custom Result Handling
 
 So far we have seen examples where geocoding results are assigned automatically to predefined object attributes. However, you can skip the auto-assignment by providing a block which handles the parsed geocoding results any way you like, for example:
 
-    reverse_geocoded_by :latitude, :longitude do |obj,results|
-      if geo = results.first
-        obj.city    = geo.city
-        obj.zipcode = geo.postal_code
-        obj.country = geo.country_code
-      end
-    end
-    after_validation :reverse_geocode
+```ruby
+reverse_geocoded_by :latitude, :longitude do |obj,results|
+  if geo = results.first
+    obj.city    = geo.city
+    obj.zipcode = geo.postal_code
+    obj.country = geo.country_code
+  end
+end
+
+after_validation :reverse_geocode
+```
 
 Every `Geocoder::Result` object, `result`, provides the following data:
 
@@ -392,23 +460,26 @@ You can apply both forward and reverse geocoding to the same model (i.e. users c
 
 For example:
 
-    class Venue
-
-      # build an address from street, city, and state attributes
-      geocoded_by :address_from_components
+```ruby
+class Venue
+  # build an address from street, city, and state attributes
+  geocoded_by :address_from_components
 
-      # store the fetched address in the full_address attribute
-      reverse_geocoded_by :latitude, :longitude, address: :full_address
-    end
+  # store the fetched address in the full_address attribute
+  reverse_geocoded_by :latitude, :longitude, address: :full_address
+end
+```
 
 The same goes for latitude/longitude. However, for purposes of querying the database, there can be only one authoritative set of latitude/longitude attributes for use in database queries. This is whichever you specify last. For example, here the attributes *without* the `fetched_` prefix will be authoritative:
 
-    class Venue
-      geocoded_by :address,
-        latitude: :fetched_latitude,
-        longitude: :fetched_longitude
-      reverse_geocoded_by :latitude, :longitude
-    end
+```ruby
+class Venue
+  geocoded_by :address,
+    latitude: :fetched_latitude,
+    longitude: :fetched_longitude
+  reverse_geocoded_by :latitude, :longitude
+end
+```
 
 
 Advanced Database Queries
@@ -418,21 +489,29 @@ Advanced Database Queries
 
 The default `near` search looks for objects within a circle. To search within a doughnut or ring use the `:min_radius` option:
 
-    Venue.near("Austin, TX", 200, min_radius: 40)
+```ruby
+Venue.near("Austin, TX", 200, min_radius: 40)
+```
 
 To search within a rectangle (note that results will *not* include `distance` and `bearing` attributes):
 
-    sw_corner = [40.71, 100.23]
-    ne_corner = [36.12, 88.65]
-    Venue.within_bounding_box(sw_corner, ne_corner)
+```ruby
+sw_corner = [40.71, 100.23]
+ne_corner = [36.12, 88.65]
+Venue.within_bounding_box(sw_corner, ne_corner)
+```
 
 To search for objects near a certain point where each object has a different distance requirement (which is defined in the database), you can pass a column name for the radius:
 
-    Venue.near([40.71, 99.23], :effective_radius)
+```ruby
+Venue.near([40.71, 99.23], :effective_radius)
+```
 
 If you store multiple sets of coordinates for each object, you can specify latitude and longitude columns to use for a search:
 
-    Venue.near("Paris", 50, latitude: :secondary_latitude, longitude: :secondary_longitude)
+```ruby
+Venue.near("Paris", 50, latitude: :secondary_latitude, longitude: :secondary_longitude)
+```
 
 ### Distance and Bearing
 
@@ -452,9 +531,11 @@ Results are automatically sorted by distance from the search point, closest to f
 
 You can convert these to compass point names via provided method:
 
-    Geocoder::Calculations.compass_point(355) # => "N"
-    Geocoder::Calculations.compass_point(45)  # => "NE"
-    Geocoder::Calculations.compass_point(208) # => "SW"
+```ruby
+Geocoder::Calculations.compass_point(355) # => "N"
+Geocoder::Calculations.compass_point(45)  # => "NE"
+Geocoder::Calculations.compass_point(208) # => "SW"
+```
 
 _Note: when running queries on SQLite, `distance` and `bearing` are provided for consistency only. They are not very accurate._
 
@@ -466,13 +547,15 @@ Geospatial Calculations
 
 The `Geocoder::Calculations` module contains some useful methods:
 
-    # find the distance between two arbitrary points
-    Geocoder::Calculations.distance_between([47.858205,2.294359], [40.748433,-73.985655])
-     => 3619.77359999382 # in configured units (default miles)
+```ruby
+# find the distance between two arbitrary points
+Geocoder::Calculations.distance_between([47.858205,2.294359], [40.748433,-73.985655])
+ => 3619.77359999382 # in configured units (default miles)
 
-    # find the geographic center (aka center of gravity) of objects or points
-    Geocoder::Calculations.geographic_center([city1, city2, [40.22,-73.99], city4])
-     => [35.14968, -90.048929]
+# find the geographic center (aka center of gravity) of objects or points
+Geocoder::Calculations.geographic_center([city1, city2, [40.22,-73.99], city4])
+ => [35.14968, -90.048929]
+```
 
 See [the code](https://github.com/alexreisner/geocoder/blob/master/lib/geocoder/calculations.rb) for more!
 
@@ -482,19 +565,27 @@ Batch Geocoding
 
 If you have just added geocoding to an existing application with a lot of objects, you can use this Rake task to geocode them all:
 
-    rake geocode:all CLASS=YourModel
+```sh
+rake geocode:all CLASS=YourModel
+```
 
 If you need reverse geocoding instead, call the task with REVERSE=true:
 
-    rake geocode:all CLASS=YourModel REVERSE=true
+```sh
+rake geocode:all CLASS=YourModel REVERSE=true
+```
 
 In either case, it won't try to geocode objects that are already geocoded. The task will print warnings if you exceed the rate limit for your geocoding service. Some services enforce a per-second limit in addition to a per-day limit. To avoid exceeding the per-second limit, you can add a `SLEEP` option to pause between requests for a given amount of time. You can also load objects in batches to save memory, for example:
 
-    rake geocode:all CLASS=YourModel SLEEP=0.25 BATCH=100
+```sh
+rake geocode:all CLASS=YourModel SLEEP=0.25 BATCH=100
+```
 
 To avoid exceeding per-day limits you can add a `LIMIT` option. However, this will ignore the `BATCH` value, if provided.
 
-    rake geocode:all CLASS=YourModel LIMIT=1000
+```sh
+rake geocode:all CLASS=YourModel LIMIT=1000
+```
 
 
 Testing
@@ -502,42 +593,54 @@ Testing
 
 When writing tests for an app that uses Geocoder it may be useful to avoid network calls and have Geocoder return consistent, configurable results. To do this, configure the `:test` lookup and/or `:ip_lookup`
 
-    Geocoder.configure(lookup: :test, ip_lookup: :test)
+```ruby
+Geocoder.configure(lookup: :test, ip_lookup: :test)
+```
 
 Add stubs to define the results that will be returned:
 
-    Geocoder::Lookup::Test.add_stub(
-      "New York, NY", [
-        {
-          'coordinates'  => [40.7143528, -74.0059731],
-          'address'      => 'New York, NY, USA',
-          'state'        => 'New York',
-          'state_code'   => 'NY',
-          'country'      => 'United States',
-          'country_code' => 'US'
-        }
-      ]
-    )
+```ruby
+Geocoder::Lookup::Test.add_stub(
+  "New York, NY", [
+    {
+      'coordinates'  => [40.7143528, -74.0059731],
+      'address'      => 'New York, NY, USA',
+      'state'        => 'New York',
+      'state_code'   => 'NY',
+      'country'      => 'United States',
+      'country_code' => 'US'
+    }
+  ]
+)
+```
 
 With the above stub defined, any query for "New York, NY" will return the results array that follows. You can also set a default stub, to be returned when no other stub matches a given query:
 
-    Geocoder::Lookup::Test.set_default_stub(
-      [
-        {
-          'coordinates'  => [40.7143528, -74.0059731],
-          'address'      => 'New York, NY, USA',
-          'state'        => 'New York',
-          'state_code'   => 'NY',
-          'country'      => 'United States',
-          'country_code' => 'US'
-        }
-      ]
-    )
+```ruby
+Geocoder::Lookup::Test.set_default_stub(
+  [
+    {
+      'coordinates'  => [40.7143528, -74.0059731],
+      'address'      => 'New York, NY, USA',
+      'state'        => 'New York',
+      'state_code'   => 'NY',
+      'country'      => 'United States',
+      'country_code' => 'US'
+    }
+  ]
+)
+```
+
+You may also delete a single stub, or reset all stubs _including the default stub_:
+
+```ruby
+Geocoder::Lookup::Test.delete_stub('New York, NY')
+Geocoder::Lookup::Test.reset
+```
 
 Notes:
 
 - Keys must be strings (not symbols) when calling `add_stub` or `set_default_stub`. For example `'country' =>` not `:country =>`.
-- To clear stubs (e.g. prior to another spec), use `Geocoder::Lookup::Test.reset`. This will clear all stubs _including the default stub_.
 - The stubbed result objects returned by the Test lookup do not support all the methods real result objects do. If you need to test interaction with real results it may be better to use an external stubbing tool and something like WebMock or VCR to prevent network calls.
 
 
@@ -546,21 +649,27 @@ Error Handling
 
 By default Geocoder will rescue any exceptions raised by calls to a geocoding service and return an empty array. You can override this on a per-exception basis, and also have Geocoder raise its own exceptions for certain events (eg: API quota exceeded) by using the `:always_raise` option:
 
-    Geocoder.configure(always_raise: [SocketError, Timeout::Error])
+```ruby
+Geocoder.configure(always_raise: [SocketError, Timeout::Error])
+```
 
 You can also do this to raise all exceptions:
 
-    Geocoder.configure(always_raise: :all)
+```ruby
+Geocoder.configure(always_raise: :all)
+```
 
 The raise-able exceptions are:
 
-    SocketError
-    Timeout::Error
-    Geocoder::OverQueryLimitError
-    Geocoder::RequestDenied
-    Geocoder::InvalidRequest
-    Geocoder::InvalidApiKey
-    Geocoder::ServiceUnavailable
+```ruby
+SocketError
+Timeout::Error
+Geocoder::OverQueryLimitError
+Geocoder::RequestDenied
+Geocoder::InvalidRequest
+Geocoder::InvalidApiKey
+Geocoder::ServiceUnavailable
+```
 
 Note that only a few of the above exceptions are raised by any given lookup, so there's no guarantee if you configure Geocoder to raise `ServiceUnavailable` that it will actually be raised under those conditions (because most APIs don't return 503 when they should; you may get a `Timeout::Error` instead). Please see the source code for your particular lookup for details.
 
@@ -570,15 +679,17 @@ Command Line Interface
 
 When you install the Geocoder gem it adds a `geocode` command to your shell. You can search for a street address, IP address, postal code, coordinates, etc just like you can with the Geocoder.search method for example:
 
-    $ geocode 29.951,-90.081
-    Latitude:         29.952211
-    Longitude:        -90.080563
-    Full address:     1500 Sugar Bowl Dr, New Orleans, LA 70112, USA
-    City:             New Orleans
-    State/province:   Louisiana
-    Postal code:      70112
-    Country:          United States
-    Map:              http://maps.google.com/maps?q=29.952211,-90.080563
+```sh
+$ geocode 29.951,-90.081
+Latitude:         29.952211
+Longitude:        -90.080563
+Full address:     1500 Sugar Bowl Dr, New Orleans, LA 70112, USA
+City:             New Orleans
+State/province:   Louisiana
+Postal code:      70112
+Country:          United States
+Map:              http://maps.google.com/maps?q=29.952211,-90.080563
+```
 
 There are also a number of options for setting the geocoding API, key, and language, viewing the raw JSON response, and more. Please run `geocode -h` for details.
 
@@ -614,8 +725,10 @@ Troubleshooting
 
 If you get one of these errors:
 
-    uninitialized constant Geocoder::Model::Mongoid
-    uninitialized constant Geocoder::Model::Mongoid::Mongo
+```ruby
+uninitialized constant Geocoder::Model::Mongoid
+uninitialized constant Geocoder::Model::Mongoid::Mongo
+```
 
 you should check your Gemfile to make sure the Mongoid gem is listed _before_ Geocoder. If Mongoid isn't loaded when Geocoder is initialized, Geocoder will not load support for Mongoid.
 
@@ -634,19 +747,23 @@ If your application requires quick geocoding responses you will probably need to
 
 For IP address lookups in Rails applications, it is generally NOT a good idea to run `request.location` during a synchronous page load without understanding the speed/behavior of your configured lookup. If the lookup becomes slow, so will your website.
 
-For the most part, the speed of geocoding requests has little to do with the Geocoder gem. Please take the time to learn about your configured lookup (links to documentation are provided above) before posting performance-related issues.
+For the most part, the speed of geocoding requests has little to do with the Geocoder gem. Please take the time to learn about your configured lookup before posting performance-related issues.
 
 ### Unexpected Responses from Geocoding Services
 
 Take a look at the server's raw response. You can do this by getting the request URL in an app console:
 
-    Geocoder::Lookup.get(:nominatim).query_url(Geocoder::Query.new("..."))
+```ruby
+Geocoder::Lookup.get(:nominatim).query_url(Geocoder::Query.new("..."))
+```
 
 Replace `:nominatim` with the lookup you are using and replace `...` with the address you are trying to geocode. Then visit the returned URL in your web browser. Often the API will return an error message that helps you resolve the problem. If, after reading the raw response, you believe there is a problem with Geocoder, please post an issue and include both the URL and raw response body.
 
 You can also fetch the response in the console:
 
-    Geocoder::Lookup.get(:nominatim).send(:fetch_raw_data, Geocoder::Query.new("..."))
+```ruby
+Geocoder::Lookup.get(:nominatim).send(:fetch_raw_data, Geocoder::Query.new("..."))
+```
 
 
 Known Issues
@@ -662,14 +779,16 @@ You cannot use the `near` scope with another scope that provides an `includes` o
 
 Instead of using `includes` to reduce the number of database queries, try using `joins` with either the `:select` option or a call to `preload`. For example:
 
-    # Pass a :select option to the near scope to get the columns you want.
-    # Instead of City.near(...).includes(:venues), try:
-    City.near("Omaha, NE", 20, select: "cities.*, venues.*").joins(:venues)
+```ruby
+# Pass a :select option to the near scope to get the columns you want.
+# Instead of City.near(...).includes(:venues), try:
+City.near("Omaha, NE", 20, select: "cities.*, venues.*").joins(:venues)
 
-    # This preload call will normally trigger two queries regardless of the
-    # number of results; one query on hotels, and one query on administrators.
-    # Instead of Hotel.near(...).includes(:administrator), try:
-    Hotel.near("London, UK", 50).joins(:administrator).preload(:administrator)
+# This preload call will normally trigger two queries regardless of the
+# number of results; one query on hotels, and one query on administrators.
+# Instead of Hotel.near(...).includes(:administrator), try:
+Hotel.near("London, UK", 50).joins(:administrator).preload(:administrator)
+```
 
 If anyone has a more elegant solution to this problem I am very interested in seeing it.
 
@@ -678,29 +797,4 @@ If anyone has a more elegant solution to this problem I am very interested in se
 The `near` method will not look across the 180th meridian to find objects close to a given point. In practice this is rarely an issue outside of New Zealand and certain surrounding islands. This problem does not exist with the zero-meridian. The problem is due to a shortcoming of the Haversine formula which Geocoder uses to calculate distances.
 
 
-Reporting Issues
-----------------
-
-When reporting an issue, please list the version of Geocoder you are using and any relevant information about your application (Rails version, database type and version, etc). Please describe as specifically as you can what behavior you are seeing (eg: an error message? a nil return value?).
-
-Please DO NOT use GitHub issues to ask questions about how to use Geocoder. Sites like [StackOverflow](http://www.stackoverflow.com/) are a better forum for such discussions.
-
-
-Contributing
-------------
-
-Contributions are welcome via Github pull requests. If you are new to the project and looking for a way to get involved, try picking up an issue with a "beginner-task" label. Hints about what needs to be done are usually provided.
-
-For all contributions, please respect the following guidelines:
-
-* Each pull request should implement ONE feature or bugfix. If you want to add or fix more than one thing, submit more than one pull request.
-* Do not commit changes to files that are irrelevant to your feature or bugfix (eg: `.gitignore`).
-* Do not add dependencies on other gems.
-* Do not add unnecessary `require` statements which could cause LoadErrors on certain systems.
-* Remember: Geocoder needs to run outside of Rails. Don't assume things like ActiveSupport are available.
-* Be willing to accept criticism and work on improving your code; Geocoder is used by thousands of developers and care must be taken not to introduce bugs.
-* Be aware that the pull request review process is not immediate, and is generally proportional to the size of the pull request.
-* If your pull request is merged, please do not ask for an immediate release of the gem. There are many factors contributing to when releases occur (remember that they affect thousands of apps with Geocoder in their Gemfiles). If necessary, please install from the Github source until the next official release.
-
-
-Copyright (c) 2009-18 Alex Reisner, released under the MIT license.
+Copyright :copyright: 2009-2021 Alex Reisner, released under the MIT license.
diff --git a/README_API_GUIDE.md b/README_API_GUIDE.md
new file mode 100644
index 0000000..6499280
--- /dev/null
+++ b/README_API_GUIDE.md
@@ -0,0 +1,792 @@
+Guide to Geocoding APIs
+=======================
+
+This is a list of geocoding APIs supported by the Geocoder gem. Before using any API in a production environment, please read its official Terms of Service (links below).
+
+Table of Contents
+-----------------
+
+* [Global Street Address Lookups](#global-street-address-lookups)
+* [Regional Street Address Lookups](#regional-street-address-lookups)
+* [IP Address Lookups](#ip-address-lookups)
+* [Local IP Address Lookups](#local-ip-address-lookups)
+
+Global Street Address Lookups
+-----------------------------
+
+### Amazon Location Service (`:amazon_location_service`)
+
+* **API key**: required
+* **Key signup**: https://console.aws.amazon.com/location
+* **Quota**: pay-as-you-go pricing; 50 requests/second
+* **Region**: world
+* **SSL support**: yes, required
+* **Languages**: en
+* **Required params**:
+  * `:index_name` - the name of the place index resource you want to use for the search
+* **Extra params**:
+  * `:max_results` - return at most this many results
+* **Extra params** when geocoding (not reverse geocoding):
+    * `:bias_position` - bias the results toward a given point, defined as `[latitude, longitude]`
+    * `:filter_b_box` - a bounding box that you specify to filter your results to coordinates within the box's boundaries, defined as `[longitude_sw, latitude_sw, longitude_ne, latitude_ne]`
+    * `:filter_countries` - an array of countries you want to geocode within, named by [ISO 3166 country codes](https://www.iso.org/iso-3166-country-codes.html), e.g. `['DEU', 'FRA']`
+* **Documentation**: https://docs.aws.amazon.com/location
+* **Terms of Service**: https://aws.amazon.com/service-terms
+* **Notes**:
+  * You must install either the `aws-sdk` or `aws-sdk-locationservice` gem, version 1.4.0 or greater.
+  * You can set a default index name for all queries in the Geocoder configuration:
+    ```rb
+      Geocoder.configure(
+        lookup: :amazon_location_service,
+        amazon_location_service: {
+          index_name: 'YOUR_INDEX_NAME_GOES_HERE',
+        }
+      )
+    ```
+  * You can provide credentials to the AWS SDK in multiple ways:
+    * Directly via the `api_key` parameter in the geocoder configuration:
+      ```rb
+        Geocoder.configure(
+          lookup: :amazon_location_service,
+          amazon_location_service: {
+            index_name: 'YOUR_INDEX_NAME_GOES_HERE',
+            api_key: {
+              access_key_id: 'YOUR_AWS_ACCESS_KEY_ID_GOES_HERE',
+              secret_access_key: 'YOUR_AWS_SECRET_ACCESS_KEY_GOES_HERE',
+            }
+          }
+        )
+      ```
+    * Via environment variables and other external methods. See **Setting AWS Credentials** in the [AWS SDK for Ruby Developer Guide](https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html).
+
+### Bing (`:bing`)
+
+* **API key**: required (set `Geocoder.configure(lookup: :bing, api_key: key)`)
+* **Key signup**: https://www.microsoft.com/maps/create-a-bing-maps-key.aspx
+* **Quota**: 50,0000 requests/day (Windows app), 125,000 requests/year (non-Windows app)
+* **Region**: world
+* **SSL support**: no
+* **Languages**: The preferred language of address elements in the result. Language code must be provided according to RFC 4647 standard.
+* **Documentation**: http://msdn.microsoft.com/en-us/library/ff701715.aspx
+* **Terms of Service**: http://www.microsoft.com/maps/product/terms.html
+* **Limitations**: No country codes or state names. Must be used on "public-facing, non-password protected web sites," "in conjunction with Bing Maps or an application that integrates Bing Maps."
+
+### Data Science Toolkit (`:dstk`)
+
+Data Science Toolkit provides an API whose response format is like Google's but which can be set up as a privately hosted service.
+
+* **API key**: none
+* **Quota**: No quota if you are self-hosting the service.
+* **Region**: world
+* **SSL support**: ?
+* **Languages**: en
+* **Documentation**: http://www.datasciencetoolkit.org/developerdocs
+* **Terms of Service**: http://www.datasciencetoolkit.org/developerdocs#googlestylegeocoder
+* **Limitations**: No reverse geocoding.
+* **Notes**: If you are hosting your own DSTK server you will need to configure the host name, eg: `Geocoder.configure(lookup: :dstk, dstk: {host: "localhost:4567"})`.
+
+### ESRI (`:esri`)
+
+* **API key**: optional (set `Geocoder.configure(esri: {api_key: ["client_id", "client_secret"]})`)
+* **Quota**: Required for some scenarios (see Terms of Service)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm
+* **Terms of Service**: http://www.esri.com/legal/software-license
+* **Limitations**: Requires API key if results will be stored. Using API key will also remove rate limit.
+* **Notes**: You can specify which projection you want to use by setting, for example: `Geocoder.configure(esri: {outSR: 102100})`. If you will store results, set the flag and provide API key: `Geocoder.configure(esri: {api_key: ["client_id", "client_secret"], for_storage: true})`. If you want to, you can also supply an ESRI token directly: `Geocoder.configure(esri: {token: Geocoder::EsriToken.new('TOKEN', Time.now + 1.day})`
+
+### Geoapify (`:geoapify`)
+
+* **API key**: required (set `Geocoder.configure(lookup: :geoapify, api_key: "your_api_key")`)
+* **Key signup**: https://myprojects.geoapify.com/register
+* **Quota**: 100,000/month with free API key, more with paid keys (see https://www.geoapify.com/api-pricing/)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: The preferred language of address elements in the result. Language code must be provided according to ISO 639-1 2-character language codes.
+* **Extra query options**:
+    * `:limit` - restrict the maximum amount of returned results, e.g. `limit: 5`
+    * `:autocomplete` - Use the automplete API, only when doing forward geocoding e.g. `autocomplete: true`
+* **Extra params** (see [Geoapify documentation](https://apidocs.geoapify.com/docs/geocoding) for more information)
+    * `:type` - restricts the type of the results, see API documentation for
+      available types, e.g. `params: { type: 'amenity' }`
+    * `:filter` - filters results by country, boundary or circle, e.g.
+      `params: { filter: 'countrycode:de,es,fr' }`, see API documentation
+      for available filters
+    * `:bias` - a location bias based on which results are prioritized, e.g.
+      `params: { bias: 'countrycode:de,es,fr' }`, see API documentation for
+      available biases
+* **Documentation**: https://apidocs.geoapify.com/docs/geocoding
+* **Terms of Service**: https://www.geoapify.com/term-and-conditions/
+* **Limitations**: When using the free plan for a commercial product, a link back is required (see https://www.geoapify.com/geocoding-api/). Rate limit (requests/second) applied based on pricing plan. [Data licensed under Open Database License (ODbL) (you must provide attribution).](https://www.openstreetmap.org/copyright)
+* **Notes**: To use Geoapify, set `Geocoder.configure(lookup: :geoapify, api_key: "your_api_key")`.
+
+### Google (`:google`)
+
+* **API key**: required
+* **Key signup**: https://developers.google.com/maps/documentation/geocoding/usage-and-billing
+* **Quota**: pay-as-you-go pricing; 50 requests/second
+* **Region**: world
+* **SSL support**: yes (required if key is used)
+* **Languages**: see https://developers.google.com/maps/faq#languagesupport
+* **Extra params**:
+  * `:bounds` - pass SW and NE coordinates as an array of two arrays to bias results towards a viewport
+  * `:google_place_id` - pass `true` if search query is a Google Place ID
+* **Documentation**: https://developers.google.com/maps/documentation/geocoding/intro
+* **Terms of Service**: http://code.google.com/apis/maps/terms.html#section_10_12
+* **Limitations**: "You can display Geocoding API results on a Google Map, or without a map. If you want to display Geocoding API results on a map, then these results must be displayed on a Google Map. ... If your application displays data from the Geocoding API on a page or view that does not also display a Google Map, you must show a Powered by Google logo with that data. ... ...you must not pre-fetch, index, store, or cache any Content except under the limited conditions stated in the terms." (see: https://developers.google.com/maps/documentation/geocoding/policies)
+
+### Google Maps API for Work (`:google_premier`)
+
+Similar to `:google`, with the following differences:
+
+* **API key**: required, plus client and channel (set `Geocoder.configure(lookup: :google_premier, api_key: [key, client, channel])`)
+* **Key signup**: https://developers.google.com/maps/premium/
+* **Quota**: 100,000 requests/24 hrs, 10 requests/second
+
+### Google Places Details (`:google_places_details`)
+
+The [Google Places Details API](https://developers.google.com/maps/documentation/places/web-service/details) is not, strictly speaking, a geocoding service. It accepts a Google `place_id` and returns address information, ratings and reviews. A `place_id` can be obtained from the Google Places Search lookup (`:google_places_search`) and should be passed to Geocoder as the first search argument: `Geocoder.search("ChIJhRwB-yFawokR5Phil-QQ3zM", lookup: :google_places_details)`.
+
+* **API key**: required
+* **Key signup**: https://code.google.com/apis/console/
+* **Quota**: 1,000 request/day, 100,000 after credit card authentication
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: ar, eu, bg, bn, ca, cs, da, de, el, en, en-AU, en-GB, es, eu, fa, fi, fil, fr, gl, gu, hi, hr, hu, id, it, iw, ja, kn, ko, lt, lv, ml, mr, nl, no, pl, pt, pt-BR, pt-PT, ro, ru, sk, sl, sr, sv, tl, ta, te, th, tr, uk, vi, zh-CN, zh-TW (see http://spreadsheets.google.com/pub?key=p9pdwsai2hDMsLkXsoM05KQ&gid=1)
+* **Extra params**:
+  * `:fields` - Requested API response fields (affects pricing, see the [Google Places Details developer guide](https://developers.google.com/maps/documentation/places/web-service/details#fields) for available fields)
+* **Documentation**: https://developers.google.com/maps/documentation/places/web-service/details
+* **Terms of Service**: https://developers.google.com/maps/documentation/places/web-service/policies
+* **Limitations**: "If your application displays Places API data on a page or view that does not also display a Google Map, you must show a "Powered by Google" logo with that data."
+* **Notes**:
+  * You can set the default fields for all queries in the Geocoder configuration, for example:
+    ```rb
+    Geocoder.configure(
+      google_places_details: {
+        fields: %w[business_status formatted_address geometry name photos place_id plus_code types]
+      }
+    )
+    ```
+
+### Google Places Search (`:google_places_search`)
+
+The [Google Places Search API](https://developers.google.com/maps/documentation/places/web-service/search) is the geocoding service of Google Places API. It returns very limited location data, but it also returns a `place_id` which can be used with Google Place Details to get more detailed information. For a comparison between this and the regular Google Geocoding API, see https://maps-apis.googleblog.com/2016/11/address-geocoding-in-google-maps-apis.html
+
+* **API key**: required
+* **Key signup**: https://code.google.com/apis/console/
+* **Quota**: 1,000 request/day, 100,000 after credit card authentication
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: ar, eu, bg, bn, ca, cs, da, de, el, en, en-AU, en-GB, es, eu, fa, fi, fil, fr, gl, gu, hi, hr, hu, id, it, iw, ja, kn, ko, lt, lv, ml, mr, nl, no, pl, pt, pt-BR, pt-PT, ro, ru, sk, sl, sr, sv, tl, ta, te, th, tr, uk, vi, zh-CN, zh-TW (see http://spreadsheets.google.com/pub?key=p9pdwsai2hDMsLkXsoM05KQ&gid=1)
+* **Extra params**:
+  * `:fields` - requested API response fields (affects pricing, see the [source](https://github.com/alexreisner/geocoder/blob/master/lib/geocoder/lookups/google_places_search.rb) for available fields)
+  * `:locationbias` - bias towards results in or near a specified area, using a string in one of the formats specified in the [API documentation](https://developers.google.com/maps/documentation/places/web-service/search-find-place#locationbias), e.g., `locationbias: "point:-36.8509,174.7645"`
+* **Documentation**: https://developers.google.com/maps/documentation/places/web-service/search
+* **Terms of Service**: https://developers.google.com/maps/documentation/places/web-service/policies
+* **Limitations**: "If your application displays Places API data on a page or view that does not also display a Google Map, you must show a "Powered by Google" logo with that data."
+* **Notes**:
+  * You can set the default fields and/or location bias for all queries in the Geocoder configuration, for example:
+    ```rb
+    Geocoder.configure(
+      google_places_search: {
+        fields: %w[address_components adr_address business_status formatted_address geometry name
+            photos place_id plus_code types url utc_offset vicinity],
+        locationbias: "point:-36.8509,174.7645"
+      }
+    )
+    ```
+
+### Here/Nokia (`:here`)
+
+* **API key**: required
+* **Quota**: Depending on the API key
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: The preferred language of address elements in the result. Language code must be provided according to RFC 4647 standard.
+* **Extra params**:
+  * `:country` - pass the country or list of countries using the country code (3 bytes, ISO 3166-1-alpha-3) or the country name, to filter the results
+* **Documentation**: https://developer.here.com/documentation/geocoding-search-api/dev_guide/topics/endpoint-geocode-brief.html
+* **Terms of Service**: https://developer.here.com/terms-and-conditions
+* **Limitations**: ?
+
+### LocationIQ (`:location_iq`)
+
+* **API key**: required
+* **Quota**: 60 requests/minute (2 req/sec, 10k req/day), then [ability to purchase more](http://locationiq.com/pricing)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: ?
+* **Documentation**: https://locationiq.com/docs
+* **Terms of Service**: https://unwiredlabs.com/tos
+* **Limitations**: [Data licensed under Open Database License (ODbL) (you must provide attribution).](https://www.openstreetmap.org/copyright)
+
+### Mapbox (`:mapbox`)
+
+* **API key**: required
+* **Dataset**: Uses `mapbox.places` dataset by default.  Specify the `mapbox.places-permanent` dataset by setting: `Geocoder.configure(mapbox: {dataset: "mapbox.places-permanent"})`
+* **Key signup**: https://www.mapbox.com/pricing/
+* **Quota**: depends on plan
+* **Region**: complete coverage of US and Canada, partial coverage elsewhere (see for details: https://www.mapbox.com/developers/api/geocoding/#coverage)
+* **SSL support**: yes
+* **Languages**: English
+* **Extra params** (see Mapbox docs for more):
+    * `:country` - restrict results to a specific country, e.g., `us` or `ca`
+    * `:types` - restrict results to categories such as `address`,
+    `neighborhood`, `postcode`
+    * `:proximity` - bias results toward a `lng,lat`, e.g.,
+        `params: { proximity: "-84.0,42.5" }`
+* **Documentation**: https://www.mapbox.com/developers/api/geocoding/
+* **Terms of Service**: https://www.mapbox.com/tos/
+* **Limitations**: For `mapbox.places` dataset, must be displayed on a Mapbox map; Cache results for up to 30 days. For `mapbox.places-permanent` dataset, depends on plan.
+* **Notes**: Currently in public beta.
+
+### Mapquest (`:mapquest`)
+
+* **API key**: required
+* **Key signup**: https://developer.mapquest.com/plans
+* **Quota**: ?
+* **HTTP Headers**: when using the licensed API you can specify a referer like so:
+    `Geocoder.configure(http_headers: { "Referer" => "http://foo.com" })`
+* **Region**: world
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: http://www.mapquestapi.com/geocoding/
+* **Terms of Service**: http://info.mapquest.com/terms-of-use/
+* **Limitations**: ?
+* **Notes**: You can use the open (non-licensed) API by setting: `Geocoder.configure(mapquest: {open: true})` (defaults to licensed version)
+
+### Melissa Data (`:melissa_street`)
+
+* **API key**: required
+* **Key signup**: https://www.melissa.com/developer/
+* **Quota**: ?
+* **Region**: world
+* **Languages**: English
+* **Documentation**: https://www.melissa.com/developer/
+* **Terms of Service**: https://www.melissa.com/terms
+* **Limitations**: ?
+
+### Nominatim (`:nominatim`)
+
+* **API key**: none
+* **Quota**: 1 request/second
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: ?
+* **Documentation**: http://wiki.openstreetmap.org/wiki/Nominatim
+* **Terms of Service**: https://operations.osmfoundation.org/policies/nominatim/
+* **Limitations**: Please limit request rate to 1 per second and include your contact information in User-Agent headers (eg: `Geocoder.configure(http_headers: { "User-Agent" => "your contact info" })`). [Data licensed under Open Database License (ODbL) (you must provide attribution).](http://www.openstreetmap.org/copyright)
+
+### OpenCageData (`:opencagedata`)
+
+* **API key**: required
+* **Key signup**: https://opencagedata.com
+* **Quota**: 2500 requests / day, then [ability to purchase more](https://opencagedata.com/pricing)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: worldwide
+* **Documentation**: https://opencagedata.com/api
+* **Limitations**: [Data licensed under Open Database License (ODbL) (you must provide attribution).](http://www.openstreetmap.org/copyright)
+
+### OSM Names (`:osmnames`)
+
+Open source geocoding engine which can be self-hosted. MapTiler.com hosts an installation for use with API key.
+
+* **API key**: required if not self-hosting (see https://www.maptiler.com/cloud/plans/)
+* **Quota**: none if self-hosting; 100,000/mo with MapTiler free plan (more with paid)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://osmnames.org/ (open source project), https://cloud.maptiler.com/geocoding/ (MapTiler)
+* **Terms of Service**: https://www.maptiler.com/terms/
+* **Notes**: To use self-hosted service, set the `:host` option in `Geocoder.configure`.
+
+### Pelias (`:pelias`)
+
+Open source geocoding engine which can be self-hosted. There are multiple service providers that can host Pelias instances (see notes).
+
+* **API key**: configurable (self-hosted service)
+* **Quota**: none (self-hosted service)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: en; see https://github.com/pelias/documentation/blob/master/language-codes.md
+* **Extra params**: See [Pelias documentation](https://github.com/pelias/documentation/blob/master/search.md#available-search-parameters)
+* **Documentation**: https://github.com/pelias/documentation/
+* **Terms of Service**: https://github.com/pelias/documentation/blob/master/data-sources.md
+* **Limitations**: See service provider terms
+* **Notes**: Configure your self-hosted pelias with the `endpoint` option: `Geocoder.configure(lookup: :pelias, api_key: 'your_api_key', pelias: {endpoint: 'self.hosted/pelias'})`. Defaults to `localhost`.
+    * [Geocode Earth](https://geocode.earth/cloud) - Cleared for Takeoff, Inc. (USA)
+    * [Geoapify](https://www.geoapify.com/maps-geocoging-routing-on-premise-installations/) - Geoapify GmbH (Germany)
+
+### Photon (`:photon`)
+
+Open source geocoding engine which can be self-hosted. Komoot hosts a public installation for fair use without the need for an API key (usage might be subject of change).
+
+* **API key**: none
+* **Quota**: You can use the API for your project, but please be fair - extensive usage will be throttled.
+* **Region**: world
+* **SSL support**: yes
+* **Languages**:  en, de, fr, it
+* **Extra query options** (see [Photon documentation](https://github.com/komoot/photon) for more information):
+    * `:limit` - restrict the maximum amount of returned results, e.g. `limit: 5`
+    * `:filter` - extra filters for the search
+        * `:bbox` (forward) - restricts the bounding box for the forward search,
+          e.g. `filter: { bbox: [9.5, 51.5, 11.5, 53.5] }`
+          (minLon, minLat, maxLon, maxLat).
+        * `:osm_tag` (forward) - filters forward search results by
+          [tags and values](https://taginfo.openstreetmap.org/projects/nominatim#tags),
+          e.g. `filter: { osm_tag: 'tourism:museum' }`,
+          see API documentation for more information.
+        * `:string` (reverse) - filters the reverse search results by a query
+          string filter, e.g. `filter: { string: 'query string filter' }`,
+    * `:bias` (forward) - a location bias based on which results are
+      prioritized, provide an option hash with the keys `:latitude`,
+      `:longitude`, and `:scale` (optional, default scale: 1.6), e.g.
+      `bias: { latitude: 12, longitude: 12, scale: 4 }`
+    * `:radius` (reverse) - a kilometer radius for the reverse geocoding search,
+      must be a positive number between 0-5000 (default radius: 1),
+      e.g. `radius: 10`
+    * `:distance_sort` (reverse) - defines if results are sorted by distance for
+      reverse search queries or not, only available if the distance sorting is
+      enabled for the instace, e.g. `distance_sort: true`
+* **Documentation**: https://github.com/komoot/photon
+* **Terms of Service**: https://photon.komoot.io/
+* **Limitations**: The public API provider (Komoot) does not guarantee for the availability and usage might be subject of change in the future. You can host your own Photon server without such limitations. [Data licensed under Open Database License (ODbL) (you must provide attribution).](https://www.openstreetmap.org/copyright)
+* **Notes**: If you are [running your own instance of Photon](https://github.com/komoot/photon) you can configure the host like this: `Geocoder.configure(lookup: :photon, photon: {host: "photon.example.org"})`.
+
+### PickPoint (`:pickpoint`)
+
+* **API key**: required
+* **Key signup**: [https://pickpoint.io](https://pickpoint.io)
+* **Quota**: 2500 requests / day for free non-commercial usage, commercial plans are [available](https://pickpoint.io/#pricing). No rate limit.
+* **Region**: world
+* **SSL support**: required
+* **Languages**: worldwide
+* **Documentation**: [https://pickpoint.io/api-reference](https://pickpoint.io/api-reference)
+* **Limitations**: [Data licensed under Open Database License (ODbL) (you must provide attribution).](http://www.openstreetmap.org/copyright)
+
+### Yandex (`:yandex`)
+
+* **API key**: optional, but without it lookup is territorially limited
+* **Quota**: 25000 requests / day
+* **Region**: world with API key, else restricted to Russia, Ukraine, Belarus, Kazakhstan, Georgia, Abkhazia, South Ossetia, Armenia, Azerbaijan, Moldova, Turkmenistan, Tajikistan, Uzbekistan, Kyrgyzstan and Turkey
+* **SSL support**: HTTPS only
+* **Languages**: Russian, Belarusian, Ukrainian, English, Turkish (only for maps of Turkey)
+* **Documentation**: http://api.yandex.com.tr/maps/doc/intro/concepts/intro.xml
+* **Terms of Service**: http://api.yandex.com.tr/maps/doc/intro/concepts/intro.xml#rules
+* **Limitations**: ?
+
+
+Regional Street Address Lookups
+-------------------------------
+
+### AMap (`:amap`)
+
+- **API key**: required
+- **Quota**: 2000/day and 2000/minute for personal developer, 4000000/day and 60000/minute for enterprise developer, for geocoding requests
+- **Region**: China
+- **SSL support**: yes
+- **Languages**: Chinese (Simplified)
+- **Documentation**: http://lbs.amap.com/api/webservice/guide/api/georegeo
+- **Terms of Service**: http://lbs.amap.com/home/terms/
+- **Limitations**: Only good for non-commercial use. For commercial usage please check http://lbs.amap.com/home/terms/
+- **Notes**: To use AMap set `Geocoder.configure(lookup: :amap, api_key: "your_api_key")`.
+
+### Baidu (`:baidu`)
+
+* **API key**: required
+* **Quota**: No quota limits for geocoding
+* **Region**: China
+* **SSL support**: no
+* **Languages**: Chinese (Simplified)
+* **Documentation**: http://developer.baidu.com/map/webservice-geocoding.htm
+* **Terms of Service**: http://developer.baidu.com/map/law.htm
+* **Limitations**: Only good for non-commercial use. For commercial usage please check http://developer.baidu.com/map/question.htm#qa0013
+* **Notes**: To use Baidu set `Geocoder.configure(lookup: :baidu, api_key: "your_api_key")`.
+
+### Base Adresse Nationale FR (`:ban_data_gouv_fr`)
+
+* **API key**: none
+* **Quota**: none
+* **Region**: France
+* **SSL support**: yes
+* **Languages**: en / fr
+* **Documentation**: https://adresse.data.gouv.fr/api/ (in french)
+* **Terms of Service**: https://adresse.data.gouv.fr/faq/ (in french)
+* **Limitations**: [Data licensed under Open Database License (ODbL) (you must provide attribution).](http://openstreetmap.fr/ban)
+
+### Geocoder.ca (`:geocoder_ca`)
+
+* **API key**: none
+* **Quota**: ?
+* **Region**: US, Canada, Mexico
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: https://geocoder.ca/?premium_api=1
+* **Terms of Service**: http://geocoder.ca/?terms=1
+* **Limitations**: "Under no circumstances can our data be re-distributed or re-sold by anyone to other parties without our written permission."
+
+### Geocodio (`:geocodio`)
+
+* **API key**: required
+* **Quota**: 2,500 free requests/day then purchase $0.0005 for each, also has volume pricing and plans.
+* **Region**: US & Canada
+* **SSL support**: yes
+* **Languages**: en
+* **Documentation**: https://geocod.io/docs/
+* **Terms of Service**: https://geocod.io/terms-of-use/
+* **Limitations**: No restrictions on use
+
+### Geoportail.lu (`:geoportail_lu`)
+
+* **API key**: none
+* **Quota**: none
+* **Region**: Luxembourg
+* **SSL support**: yes
+* **Languages**: en
+* **Documentation**: http://wiki.geoportail.lu/doku.php?id=en:api
+* **Terms of Service**: http://wiki.geoportail.lu/doku.php?id=en:mcg_1
+* **Limitations**: ?
+
+### LatLon.io (`:latlon`)
+
+* **API key**: required
+* **Quota**: Depends on the user's plan (free and paid plans available)
+* **Region**: US
+* **SSL support**: yes
+* **Languages**: en
+* **Documentation**: https://latlon.io/documentation
+* **Terms of Service**: ?
+* **Limitations**: No restrictions on use
+
+### Nationaal Georegister Netherlands (`:nationaal_georegister_nl`)
+
+* **API key**: none
+* **Quota**: none
+* **Region**: Netherlands
+* **SSL support**: yes
+* **Languages**: Dutch
+* **Documentation**: http://geodata.nationaalgeoregister.nl/
+* **Terms of Service**: https://www.pdok.nl/over-pdok - The PDOK services are based on open data and are therefore freely available to everyone.
+
+### Ordnance Survey OpenNames (`:uk_ordnance_survey_names`)
+
+* **API key**: required (sign up at https://developer.ordnancesurvey.co.uk/os-names-api)
+* **Quota**: 250,000 / month
+* **Region**: England, Wales and Scotland
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://apidocs.os.uk/docs/os-names-overview
+* **Terms of Service**: https://developer.ordnancesurvey.co.uk/os-api-framework-agreement
+* **Limitations**: Only searches postcodes and placenames in England, Wales and Scotland
+
+### PostcodeAnywhere UK (`:postcode_anywhere_uk`)
+
+* **API key**: required
+* **Quota**: Dependant on service plan?
+* **Region**: UK
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: http://www.postcodeanywhere.co.uk/Support/WebService/Geocoding/UK/Geocode/2/
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: To use PostcodeAnywhere you must include an API key: `Geocoder.configure(lookup: :postcode_anywhere_uk, api_key: 'your_api_key')`.
+
+### SmartyStreets (`:smarty_streets`)
+
+* **API key**: requires auth_id and auth_token (set `Geocoder.configure(api_key: [id, token])`)
+* **Quota**: 250/month then purchase at sliding scale.
+* **Region**: US
+* **SSL support**: yes (required)
+* **Languages**: en
+* **Documentation**: http://smartystreets.com/kb/liveaddress-api/rest-endpoint
+* **Terms of Service**: http://smartystreets.com/legal/terms-of-service
+* **Limitations**: No reverse geocoding.
+
+### Tencent (`:tencent`)
+
+* **API key**: required
+* **Key signup**: http://lbs.qq.com/console/mykey.html
+* **Quota**: 10,000 free requests per day per key. 5 requests per second per key. For increased quota, one must first apply to become a corporate developer and then apply for increased quota.
+* **Region**: China
+* **SSL support**: yes
+* **Languages**: Chinese (Simplified)
+* **Documentation**: http://lbs.qq.com/webservice_v1/guide-geocoder.html (Standard) & http://lbs.qq.com/webservice_v1/guide-gcoder.html (Reverse)
+* **Terms of Service**: http://lbs.qq.com/terms.html
+* **Limitations**: Only works for locations in Greater China (mainland China, Hong Kong, Macau, and Taiwan).
+* **Notes**: To use Tencent, set `Geocoder.configure(lookup: :tencent, api_key: "your_api_key")`.
+
+
+IP Address Lookups
+------------------
+
+### Abstract API (`:abstract_api`)
+
+* **API key**: required
+* **Quota**: 20,000/day with free API Key, and un to 20,000,000/day for paid API keys
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://www.abstractapi.com/ip-geolocation-api#docs
+* **Terms of Service**: https://www.abstractapi.com/legal
+
+### Baidu IP (`:baidu_ip`)
+
+* **API key**: required
+* **Quota**: No quota limits for geocoding
+* **Region**: China
+* **SSL support**: no
+* **Languages**: Chinese (Simplified)
+* **Documentation**: http://developer.baidu.com/map/webservice-geocoding.htm
+* **Terms of Service**: http://developer.baidu.com/map/law.htm
+* **Limitations**: Only good for non-commercial use. For commercial usage please check http://developer.baidu.com/map/question.htm#qa0013
+* **Notes**: To use Baidu set `Geocoder.configure(lookup: :baidu_ip, api_key: "your_api_key")`.
+
+### DB-IP.com (`:db_ip_com`)
+
+* **API key**: required
+* **Quota**: 2,500/day (with free API Key, 50,000/day and up for paid API keys)
+* **Region**: world
+* **SSL support**: yes (with paid API keys - see https://db-ip.com/api/)
+* **Languages**: English (English with free API key, multiple languages with paid API keys)
+* **Documentation**: https://db-ip.com/api/doc.php
+* **Terms of Service**: https://db-ip.com/tos.php
+
+### FreeGeoIP (`:freegeoip`)
+
+* **API key**: required
+* **Quota**: 15,000 requests per hour
+* **Region**: world
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: https://github.com/apilayer/freegeoip/ and https://freegeoip.app/
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: The default host is freegeoip.app but this can be changed by using, for example, `Geocoder.configure(freegeoip: {host: 'api.ipstack.com'})`. The service can also be self-hosted.
+
+### IPBase (`:ipbase`)
+
+* **API key**: required
+* **Quota**: 10/minute up to 150 per month for free, paid plans too!
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://ipbase.com/docs
+* **Terms of Service**: https://www.iubenda.com/terms-and-conditions/41661719
+
+### IP-API.com (`:ipapi_com`)
+
+* **API key**: optional - see https://members.ip-api.com
+* **Quota**: 45/minute - unlimited with api key
+* **Region**: world
+* **SSL support**: no (not without access key - see https://members.ip-api.com)
+* **Languages**: English
+* **Documentation**: http://ip-api.com/docs/
+* **Terms of Service**: https://members.ip-api.com/legal
+
+### IP2Location (`:ip2location`)
+
+* **API key**: required (5,000 free credits given on signup; free demo key available for 20 queries per day)
+* **Quota**: up to 100k credits with paid API key
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://www.ip2location.com/web-service
+* **Terms of Service**: https://www.ip2location.com/web-service
+* **Notes**: With the non-free version, specify your desired package: `Geocoder.configure(ip2location: {package: "WSX"})` (see API documentation for package details).
+
+### Ipdata.co (`:ipdata_co`)
+
+* **API key**: required, see: https://ipdata.co/pricing.html
+* **Quota**: 1500/day for free, up to 600k with paid API keys
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://ipdata.co/docs.html
+* **Terms of Service**: https://ipdata.co/terms.html
+* **Limitations**: ?
+
+### Ipgeolocation (`:ipgeolocation`)
+
+* **API key**: required (see https://ipgeolocation.io/pricing)
+* **Quota**: 1500/day (with free API Key)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English, German, Russian, Japanese, French, Chinese, Spanish, Czech, Italian
+* **Documentation**: https://ipgeolocation.io/documentation
+* **Terms of Service**: https://ipgeolocation/tos
+* **Notes**: To use Ipgeolocation set `Geocoder.configure(ip_lookup: :ipgeolocation, api_key: "your_ipgeolocation_api_key", use_https:true)`. Supports the optional params:  { excludes: "continent_code"}, {fields: "geo"}, {lang: "ru"}, {output: "xml"}, {include: "hostname"}, {ip: "174.7.116.0"}) (see API documentation for details).
+
+### IPInfo.io (`:ipinfo_io`)
+
+* **API key**: optional - see https://ipinfo.io/pricing
+* **Quota**: 1,000/day without API key, 50,000/mo with a free account - more with a paid plan - see https://ipinfo.io/developers#rate-limits
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://ipinfo.io/developers
+* **Terms of Service**: https://ipinfo.io/terms-of-service
+
+### IPQualityScore (`:ipqualityscore`)
+
+* **API key**: required - see https://www.ipqualityscore.com/free-ip-lookup-proxy-vpn-test
+* **Quota**: 5,000/month with a free account - more with a paid plan - see https://www.ipqualityscore.com/plans
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://www.ipqualityscore.com/documentation/overview
+* **Terms of Service**: https://www.ipqualityscore.com/terms-of-service
+
+### Ipregistry (`:ipregistry`)
+
+* **API key**: required (see https://ipregistry.co)
+* **Quota**: first 100,000 requests are free, then you pay per request (see https://ipregistry.co/pricing)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://ipregistry.co/docs
+* **Terms of Service**: https://ipregistry.co/terms
+
+### Ipstack (`:ipstack`)
+
+* **API key**: required (see https://ipstack.com/product)
+* **Quota**: 100 requests per month (with free API Key, 50,000/day and up for paid plans)
+* **Region**: world
+* **SSL support**: yes ( only with paid plan )
+* **Languages**: English, German, Spanish, French, Japanese, Portugues (Brazil), Russian, Chinese
+* **Documentation**: https://ipstack.com/documentation
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: To use Ipstack set `Geocoder.configure(ip_lookup: :ipstack, api_key: "your_ipstack_api_key")`. Supports the optional params: `:hostname`, `:security`, `:fields`, `:language` (see API documentation for details).
+
+### MaxMind Legacy Web Services (`:maxmind`)
+
+* **API key**: required
+* **Quota**: Request Packs can be purchased
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: http://dev.maxmind.com/geoip/legacy/web-services/
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: You must specify which MaxMind service you are using in your configuration. For example: `Geocoder.configure(maxmind: {service: :omni})`.
+
+### MaxMind GeoIP2 Precision Web Services (`:maxmind_geoip2`)
+
+* **API key**: required
+* **Quota**: Request Packs can be purchased
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: http://dev.maxmind.com/geoip/geoip2/web-services/
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: You must specify which MaxMind service you are using in your configuration, and also basic authentication. For example: `Geocoder.configure(maxmind_geoip2: {service: :country, basic_auth: {user: '', password: ''}})`.
+
+### Pointpin (`:pointpin`)
+
+* **API key**: required
+* **Quota**: 50,000/mo for €9 through 1m/mo for €49
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://pointp.in/docs/get-started
+* **Terms of Service**: https://pointp.in/terms
+* **Limitations**: ?
+* **Notes**: To use Pointpin set `Geocoder.configure(ip_lookup: :pointpin, api_key: "your_pointpin_api_key")`.
+
+### Telize (`:telize`)
+
+* **API key**: required
+* **Quota**: 1,000/day for $7/mo through 100,000/day for $100/mo
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://rapidapi.com/fcambus/api/telize
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: To use Telize set `Geocoder.configure(ip_lookup: :telize, api_key: "your_api_key")`. Or configure your self-hosted telize with the `host` option: `Geocoder.configure(ip_lookup: :telize, telize: {host: "localhost"})`.
+
+### 2GIS (`:twogis`)
+
+* **API key**: required
+* **Key signup**:
+* **Quota**:
+* **Region**:
+* **SSL support**: required
+* **Languages**: ru_RU, ru_KG, ru_UZ, uk_UA, en_AE, it_RU, es_RU, ar_AE, cs_CZ, az_AZ, en_SA, en_EG, en_OM, en_QA, en_BH
+* **Documentation**: https://docs.2gis.com/en/api/search/geocoder/overview
+* **Terms of Service**:
+* **Limitations**:
+
+
+Local IP Address Lookups
+------------------------
+
+### GeoLite2 (`:geoip2`)
+
+This lookup provides methods for geocoding IP addresses without making a call to a remote API (improves speed and availability). It works, but support is new and should not be considered production-ready. Please [report any bugs](https://github.com/alexreisner/geocoder/issues) you encounter.
+
+* **API key**: none (requires a GeoIP2 or free GeoLite2 City or Country binary database which can be downloaded from [MaxMind](http://dev.maxmind.com/geoip/geoip2/))
+* **Quota**: none
+* **Region**: world
+* **SSL support**: N/A
+* **Languages**: English
+* **Documentation**: http://www.maxmind.com/en/city
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: **You must download a binary database file from MaxMind and set the `:file` configuration option.** The CSV format databases are not yet supported since they are still in alpha stage. Set the path to the database file in your configuration:
+
+    Geocoder.configure(
+      ip_lookup: :geoip2,
+      geoip2: {
+        file: File.join('folder', 'GeoLite2-City.mmdb')
+      }
+    )
+
+You must add either the *[hive_geoip2](https://rubygems.org/gems/hive_geoip2)* gem (native extension that relies on libmaxminddb) or the *[maxminddb](http://rubygems.org/gems/maxminddb)* gem (pure Ruby implementation) to your Gemfile or have it installed in your system. The pure Ruby gem (maxminddb) will be used by default. To use `hive_geoip2`:
+
+    Geocoder.configure(
+      ip_lookup: :geoip2,
+      geoip2: {
+        lib: 'hive_geoip2',
+        file: File.join('folder', 'GeoLite2-City.mmdb')
+      }
+    )
+
+
+### MaxMind Local (`:maxmind_local`) - EXPERIMENTAL
+
+This lookup provides methods for geocoding IP addresses without making a call to a remote API (improves speed and availability). It works, but support is new and should not be considered production-ready. Please [report any bugs](https://github.com/alexreisner/geocoder/issues) you encounter.
+
+* **API key**: none (requires the GeoLite City database which can be downloaded from [MaxMind](http://dev.maxmind.com/geoip/legacy/geolite/))
+* **Quota**: none
+* **Region**: world
+* **SSL support**: N/A
+* **Languages**: English
+* **Documentation**: http://www.maxmind.com/en/city
+* **Terms of Service**: ?
+* **Limitations**: ?
+* **Notes**: There are two supported formats for MaxMind local data: binary file, and CSV file imported into an SQL database. **You must download a database from MaxMind and set either the `:file` or `:package` configuration option for local lookups to work.**
+
+**To use a binary file** you must add the *geoip* (or *jgeoip* for JRuby) gem to your Gemfile or have it installed in your system, and specify the path of the MaxMind database in your configuration. For example:
+
+    Geocoder.configure(ip_lookup: :maxmind_local, maxmind_local: {file: File.join('folder', 'GeoLiteCity.dat')})
+
+**To use a CSV file** you must import it into an SQL database. The GeoLite *City* and *Country* packages are supported. Configure like so:
+
+    Geocoder.configure(ip_lookup: :maxmind_local, maxmind_local: {package: :city})
+
+You can generate ActiveRecord migrations and download and import data via provided rake tasks:
+
+    # generate migration to create tables
+    rails generate geocoder:maxmind:geolite_city
+
+    # download, unpack, and import data
+    rake geocoder:maxmind:geolite:load PACKAGE=city
+
+You can replace `city` with `country` in any of the above tasks, generators, and configurations.
+
+Copyright (c) 2009-2021 Alex Reisner, released under the MIT license.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..83de4cd
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,78 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+ACCEPTED_DB_VALUES = %w(sqlite postgres mysql)
+DATABASE_CONFIG_FILE = 'test/database.yml'
+
+def config
+  require 'yaml'
+  YAML.load(File.open(DATABASE_CONFIG_FILE))
+end
+
+namespace :db do
+  task :create do
+    if ACCEPTED_DB_VALUES.include? ENV['DB']
+      Rake::Task["db:#{ENV['DB']}:create"].invoke
+    end
+  end
+
+  task :drop do
+    if ACCEPTED_DB_VALUES.include? ENV['DB']
+      Rake::Task["db:#{ENV['DB']}:drop"].invoke
+    end
+  end
+
+  task :reset => [:drop, :create]
+
+  namespace :mysql do
+    desc 'Create the MySQL test databases'
+    task :create do
+      `mysql --user=#{config['mysql']['username']} -e "create DATABASE #{config['mysql']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci "`
+    end
+
+    desc 'Drop the MySQL test databases'
+    task :drop do
+      `mysql --user=#{config['mysql']['username']} -e "DROP DATABASE IF EXISTS #{config['mysql']['database']}"`
+    end
+  end
+
+  namespace :postgres do
+    desc 'Create the PostgreSQL test databases'
+    task :create do
+      `createdb -E UTF8 -T template0 #{config['postgres']['database']}`
+    end
+
+    desc 'Drop the PostgreSQL test databases'
+    task :drop do
+      `dropdb --if-exists #{config['postgres']['database']}`
+    end
+  end
+
+  namespace :sqlite do
+    task :drop
+    task :create
+  end
+end
+
+require 'rake/testtask'
+desc "Use DB to test with #{ACCEPTED_DB_VALUES}, otherwise test standalone"
+Rake::TestTask.new(:test) do |test|
+  Rake::Task['db:reset'].invoke if ACCEPTED_DB_VALUES.include? ENV['DB']
+  test.libs << 'lib' << 'test'
+  test.pattern = 'test/unit/**/*_test.rb'
+end
+
+Rake::TestTask.new(:integration) do |test|
+  test.libs << 'lib' << 'test'
+  test.pattern = 'test/integration/*_test.rb'
+end
+
+task :default => [:test]
+
+require 'rdoc/task'
+Rake::RDocTask.new do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title = "Geocoder #{Geocoder::VERSION}"
+  rdoc.rdoc_files.include('*.rdoc')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
diff --git a/bin/console b/bin/console
new file mode 100755
index 0000000..8a177d5
--- /dev/null
+++ b/bin/console
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+
+require 'bundler/setup'
+require 'geocoder'
+
+if File.exist?("api_keys.yml")
+  require 'yaml'
+  @api_keys = YAML.load(File.read("api_keys.yml"), symbolize_names: true)
+  Geocoder.configure(@api_keys)
+end
+
+require 'irb'
+IRB.start
diff --git a/debian/changelog b/debian/changelog
index b083519..f170e2f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+ruby-geocoder (1.8.1+git20230105.1.6f2f423-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+  * Drop patch CVE-2020-7981.patch, present upstream.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 11 Jan 2023 04:27:36 -0000
+
 ruby-geocoder (1.5.1-3) unstable; urgency=medium
 
   * Add patch to fix CVE-2020-7981 (Closes: #949870)
diff --git a/debian/patches/CVE-2020-7981.patch b/debian/patches/CVE-2020-7981.patch
deleted file mode 100644
index a844667..0000000
--- a/debian/patches/CVE-2020-7981.patch
+++ /dev/null
@@ -1,29 +0,0 @@
-From dcdc3d8675411edce3965941a2ca7c441ca48613 Mon Sep 17 00:00:00 2001
-From: Alex Reisner <alex@alexreisner.com>
-Date: Thu, 23 Jan 2020 09:08:45 -0700
-Subject: [PATCH] Sanitize lat/lon for SQL query.
-
----
- lib/geocoder/sql.rb | 8 ++++----
- 1 file changed, 4 insertions(+), 4 deletions(-)
-
---- a/lib/geocoder/sql.rb
-+++ b/lib/geocoder/sql.rb
-@@ -44,13 +44,13 @@
-     end
- 
-     def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr)
--      spans = "#{lat_attr} BETWEEN #{sw_lat} AND #{ne_lat} AND "
-+      spans = "#{lat_attr} BETWEEN #{sw_lat.to_f} AND #{ne_lat.to_f} AND "
-       # handle box that spans 180 longitude
-       if sw_lng.to_f > ne_lng.to_f
--        spans + "(#{lon_attr} BETWEEN #{sw_lng} AND 180 OR " +
--        "#{lon_attr} BETWEEN -180 AND #{ne_lng})"
-+        spans + "(#{lon_attr} BETWEEN #{sw_lng.to_f} AND 180 OR " +
-+        "#{lon_attr} BETWEEN -180 AND #{ne_lng.to_f})"
-       else
--        spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
-+        spans + "#{lon_attr} BETWEEN #{sw_lng.to_f} AND #{ne_lng.to_f}"
-       end
-     end
- 
diff --git a/debian/patches/series b/debian/patches/series
index 45067fd..e69de29 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +0,0 @@
-CVE-2020-7981.patch
diff --git a/examples/app_defined_lookup_services.rb b/examples/app_defined_lookup_services.rb
new file mode 100644
index 0000000..7875cee
--- /dev/null
+++ b/examples/app_defined_lookup_services.rb
@@ -0,0 +1,22 @@
+# To extend the Geocoder with additional lookups that come from the application,
+# not shipped with the gem, define a "child" lookup in your application, based on existing one.
+# This is required because the Geocoder::Configuration is a Singleton and stores one api key per lookup.
+
+# in app/libs/geocoder/lookup/my_preciousss.rb
+module Geocoder::Lookup
+  class MyPreciousss < Google
+  end
+end
+
+# Update Geocoder's street_services on initialize:
+# config/initializers/geocoder.rb
+Geocoder::Lookup.street_services << :my_preciousss
+
+# Override the configuration when necessary (e.g. provide separate Google API key for the account):
+Geocoder.configure(my_preciousss: { api_key: 'abcdef' })
+
+# Lastly, search using your custom lookup service/api keys
+Geocoder.search("Paris", lookup: :my_preciousss)
+
+# This is useful when we have groups of users in the application who use Google paid services
+# and we want to properly separate them and allow using individual API KEYS or timeouts.
diff --git a/examples/autoexpire_cache_dalli.rb b/examples/autoexpire_cache_dalli.rb
deleted file mode 100644
index 97b3773..0000000
--- a/examples/autoexpire_cache_dalli.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# This class implements a cache with simple delegation to the the Dalli Memcached client
-# https://github.com/mperham/dalli
-#
-# A TTL is set on initialization
-
-class AutoexpireCacheDalli
-  def initialize(store, ttl = 86400)
-    @store = store
-    @keys = 'GeocoderDalliClientKeys'
-    @ttl = ttl
-  end
-
-  def [](url)
-    res = @store.get(url)
-    res = YAML::load(res) if res.present?
-    res
-  end
-
-  def []=(url, value)
-    if value.nil?
-      del(url)
-    else
-      key_cache_add(url) if @store.add(url, YAML::dump(value), @ttl)
-    end
-    value
-  end
-
-  def keys
-    key_cache
-  end
-
-  def del(url)
-    key_cache_delete(url) if @store.delete(url)
-  end
-
-  private
-
-  def key_cache
-    the_keys = @store.get(@keys)
-    if the_keys.nil?
-      @store.add(@keys, YAML::dump([]))
-      []
-    else
-      YAML::load(the_keys)
-    end
-  end
-
-  def key_cache_add(key)
-    @store.replace(@keys, YAML::dump(key_cache << key))
-  end
-
-  def key_cache_delete(key)
-    tmp = key_cache
-    tmp.delete(key)
-    @store.replace(@keys, YAML::dump(tmp))
-  end
-end
-
-# Here Dalli is set up as on Heroku using the Memcachier gem.
-# https://devcenter.heroku.com/articles/memcachier#ruby
-# On other setups you might have to specify your Memcached server in Dalli::Client.new
-Geocoder.configure(:cache => AutoexpireCacheDalli.new(Dalli::Client.new))
diff --git a/examples/autoexpire_cache_redis.rb b/examples/autoexpire_cache_redis.rb
deleted file mode 100644
index 395a71e..0000000
--- a/examples/autoexpire_cache_redis.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# This class implements a cache with simple delegation to the Redis store, but
-# when it creates a key/value pair, it also sends an EXPIRE command with a TTL.
-# It should be fairly simple to do the same thing with Memcached.
-class AutoexpireCacheRedis
-  def initialize(store, ttl = 86400)
-    @store = store
-    @ttl = ttl
-  end
-
-  def [](url)
-    @store.get(url)
-  end
-
-  def []=(url, value)
-    @store.set(url, value)
-    @store.expire(url, @ttl)
-  end
-
-  def keys
-    @store.keys
-  end
-
-  def del(url)
-    @store.del(url)
-  end
-end
-
-Geocoder.configure(:cache => AutoexpireCacheRedis.new(Redis.new))
diff --git a/gemfiles/Gemfile.rails5.0 b/gemfiles/Gemfile.rails5.0
new file mode 100644
index 0000000..1068091
--- /dev/null
+++ b/gemfiles/Gemfile.rails5.0
@@ -0,0 +1,41 @@
+source "https://rubygems.org"
+
+group :development, :test do
+  gem 'rake'
+  gem 'mongoid', '~> 6.1.0'
+  gem 'bson_ext', platforms: :ruby
+  gem 'geoip'
+  gem 'rubyzip'
+  gem 'rails', '~> 5.0.1'
+  gem 'test-unit' # needed for Ruby >=2.2.0
+
+  platforms :jruby do
+    gem 'jruby-openssl'
+    gem 'jgeoip'
+  end
+
+  platforms :rbx do
+    gem 'rubysl', '~> 2.0'
+    gem 'rubysl-test-unit'
+  end
+end
+
+group :test do
+  platforms :ruby, :mswin, :mingw do
+    gem 'sqlite3', '~> 1.3.5'
+    gem 'sqlite_ext', '~> 1.5.0'
+  end
+
+  gem 'webmock'
+
+  platforms :ruby do
+    gem 'pg', '~> 0.18'
+    gem 'mysql2', '~> 0.3.11'
+  end
+
+  platforms :jruby do
+    gem 'jdbc-mysql'
+    gem 'jdbc-sqlite3'
+    gem 'activerecord-jdbcpostgresql-adapter'
+  end
+end
diff --git a/geocoder.gemspec b/geocoder.gemspec
index 1b90b41..ed6955a 100644
--- a/geocoder.gemspec
+++ b/geocoder.gemspec
@@ -1,26 +1,25 @@
-#########################################################
-# This file has been automatically generated by gem2tgz #
-#########################################################
 # -*- encoding: utf-8 -*-
-# stub: geocoder 1.5.1 ruby lib
+$:.push File.expand_path("../lib", __FILE__)
+require 'date'
+require "geocoder/version"
 
 Gem::Specification.new do |s|
-  s.name = "geocoder".freeze
-  s.version = "1.5.1"
-
-  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
-  s.metadata = { "changelog_uri" => "https://github.com/alexreisner/geocoder/blob/master/CHANGELOG.md", "source_code_uri" => "https://github.com/alexreisner/geocoder" } if s.respond_to? :metadata=
-  s.require_paths = ["lib".freeze]
-  s.authors = ["Alex Reisner".freeze]
-  s.date = "2019-01-23"
-  s.description = "Provides object geocoding (by street or IP address), reverse geocoding (coordinates to street address), distance queries for ActiveRecord and Mongoid, result caching, and more. Designed for Rails but works with Sinatra and other Rack frameworks too.".freeze
-  s.email = ["alex@alexreisner.com".freeze]
-  s.executables = ["geocode".freeze]
-  s.files = ["CHANGELOG.md".freeze, "LICENSE".freeze, "README.md".freeze, "bin/geocode".freeze, "examples/autoexpire_cache_dalli.rb".freeze, "examples/autoexpire_cache_redis.rb".freeze, "examples/cache_bypass.rb".freeze, "examples/reverse_geocode_job.rb".freeze, "lib/generators/geocoder/config/config_generator.rb".freeze, "lib/generators/geocoder/config/templates/initializer.rb".freeze, "lib/generators/geocoder/maxmind/geolite_city_generator.rb".freeze, "lib/generators/geocoder/maxmind/geolite_country_generator.rb".freeze, "lib/generators/geocoder/maxmind/templates/migration/geolite_city.rb".freeze, "lib/generators/geocoder/maxmind/templates/migration/geolite_country.rb".freeze, "lib/generators/geocoder/migration_version.rb".freeze, "lib/geocoder.rb".freeze, "lib/geocoder/cache.rb".freeze, "lib/geocoder/calculations.rb".freeze, "lib/geocoder/cli.rb".freeze, "lib/geocoder/configuration.rb".freeze, "lib/geocoder/configuration_hash.rb".freeze, "lib/geocoder/esri_token.rb".freeze, "lib/geocoder/exceptions.rb".freeze, "lib/geocoder/ip_address.rb".freeze, "lib/geocoder/kernel_logger.rb".freeze, "lib/geocoder/logger.rb".freeze, "lib/geocoder/lookup.rb".freeze, "lib/geocoder/lookups/amap.rb".freeze, "lib/geocoder/lookups/baidu.rb".freeze, "lib/geocoder/lookups/baidu_ip.rb".freeze, "lib/geocoder/lookups/ban_data_gouv_fr.rb".freeze, "lib/geocoder/lookups/base.rb".freeze, "lib/geocoder/lookups/bing.rb".freeze, "lib/geocoder/lookups/db_ip_com.rb".freeze, "lib/geocoder/lookups/dstk.rb".freeze, "lib/geocoder/lookups/esri.rb".freeze, "lib/geocoder/lookups/freegeoip.rb".freeze, "lib/geocoder/lookups/geocoder_ca.rb".freeze, "lib/geocoder/lookups/geocoder_us.rb".freeze, "lib/geocoder/lookups/geocodio.rb".freeze, "lib/geocoder/lookups/geoip2.rb".freeze, "lib/geocoder/lookups/geoportail_lu.rb".freeze, "lib/geocoder/lookups/google.rb".freeze, "lib/geocoder/lookups/google_places_details.rb".freeze, "lib/geocoder/lookups/google_places_search.rb".freeze, "lib/geocoder/lookups/google_premier.rb".freeze, "lib/geocoder/lookups/here.rb".freeze, "lib/geocoder/lookups/ip2location.rb".freeze, "lib/geocoder/lookups/ipapi_com.rb".freeze, "lib/geocoder/lookups/ipdata_co.rb".freeze, "lib/geocoder/lookups/ipinfo_io.rb".freeze, "lib/geocoder/lookups/ipstack.rb".freeze, "lib/geocoder/lookups/latlon.rb".freeze, "lib/geocoder/lookups/location_iq.rb".freeze, "lib/geocoder/lookups/mapbox.rb".freeze, "lib/geocoder/lookups/mapquest.rb".freeze, "lib/geocoder/lookups/maxmind.rb".freeze, "lib/geocoder/lookups/maxmind_geoip2.rb".freeze, "lib/geocoder/lookups/maxmind_local.rb".freeze, "lib/geocoder/lookups/nominatim.rb".freeze, "lib/geocoder/lookups/opencagedata.rb".freeze, "lib/geocoder/lookups/pelias.rb".freeze, "lib/geocoder/lookups/pickpoint.rb".freeze, "lib/geocoder/lookups/pointpin.rb".freeze, "lib/geocoder/lookups/postcode_anywhere_uk.rb".freeze, "lib/geocoder/lookups/postcodes_io.rb".freeze, "lib/geocoder/lookups/smarty_streets.rb".freeze, "lib/geocoder/lookups/telize.rb".freeze, "lib/geocoder/lookups/tencent.rb".freeze, "lib/geocoder/lookups/test.rb".freeze, "lib/geocoder/lookups/yandex.rb".freeze, "lib/geocoder/models/active_record.rb".freeze, "lib/geocoder/models/base.rb".freeze, "lib/geocoder/models/mongo_base.rb".freeze, "lib/geocoder/models/mongo_mapper.rb".freeze, "lib/geocoder/models/mongoid.rb".freeze, "lib/geocoder/query.rb".freeze, "lib/geocoder/railtie.rb".freeze, "lib/geocoder/request.rb".freeze, "lib/geocoder/results/amap.rb".freeze, "lib/geocoder/results/baidu.rb".freeze, "lib/geocoder/results/baidu_ip.rb".freeze, "lib/geocoder/results/ban_data_gouv_fr.rb".freeze, "lib/geocoder/results/base.rb".freeze, "lib/geocoder/results/bing.rb".freeze, "lib/geocoder/results/db_ip_com.rb".freeze, "lib/geocoder/results/dstk.rb".freeze, "lib/geocoder/results/esri.rb".freeze, "lib/geocoder/results/freegeoip.rb".freeze, "lib/geocoder/results/geocoder_ca.rb".freeze, "lib/geocoder/results/geocoder_us.rb".freeze, "lib/geocoder/results/geocodio.rb".freeze, "lib/geocoder/results/geoip2.rb".freeze, "lib/geocoder/results/geoportail_lu.rb".freeze, "lib/geocoder/results/google.rb".freeze, "lib/geocoder/results/google_places_details.rb".freeze, "lib/geocoder/results/google_places_search.rb".freeze, "lib/geocoder/results/google_premier.rb".freeze, "lib/geocoder/results/here.rb".freeze, "lib/geocoder/results/ip2location.rb".freeze, "lib/geocoder/results/ipapi_com.rb".freeze, "lib/geocoder/results/ipdata_co.rb".freeze, "lib/geocoder/results/ipinfo_io.rb".freeze, "lib/geocoder/results/ipstack.rb".freeze, "lib/geocoder/results/latlon.rb".freeze, "lib/geocoder/results/location_iq.rb".freeze, "lib/geocoder/results/mapbox.rb".freeze, "lib/geocoder/results/mapquest.rb".freeze, "lib/geocoder/results/maxmind.rb".freeze, "lib/geocoder/results/maxmind_geoip2.rb".freeze, "lib/geocoder/results/maxmind_local.rb".freeze, "lib/geocoder/results/nominatim.rb".freeze, "lib/geocoder/results/opencagedata.rb".freeze, "lib/geocoder/results/pelias.rb".freeze, "lib/geocoder/results/pickpoint.rb".freeze, "lib/geocoder/results/pointpin.rb".freeze, "lib/geocoder/results/postcode_anywhere_uk.rb".freeze, "lib/geocoder/results/postcodes_io.rb".freeze, "lib/geocoder/results/smarty_streets.rb".freeze, "lib/geocoder/results/telize.rb".freeze, "lib/geocoder/results/tencent.rb".freeze, "lib/geocoder/results/test.rb".freeze, "lib/geocoder/results/yandex.rb".freeze, "lib/geocoder/sql.rb".freeze, "lib/geocoder/stores/active_record.rb".freeze, "lib/geocoder/stores/base.rb".freeze, "lib/geocoder/stores/mongo_base.rb".freeze, "lib/geocoder/stores/mongo_mapper.rb".freeze, "lib/geocoder/stores/mongoid.rb".freeze, "lib/geocoder/version.rb".freeze, "lib/hash_recursive_merge.rb".freeze, "lib/maxmind_database.rb".freeze, "lib/tasks/geocoder.rake".freeze, "lib/tasks/maxmind.rake".freeze]
-  s.homepage = "http://www.rubygeocoder.com".freeze
-  s.licenses = ["MIT".freeze]
-  s.post_install_message = "\n\nNOTE: Geocoder's default IP address lookup has changed from FreeGeoIP.net to IPInfo.io. If you explicitly specify :freegeoip in your configuration you must choose a different IP lookup before FreeGeoIP is discontinued on July 1, 2018. If you do not explicitly specify :freegeoip you do not need to change anything.\n\n".freeze
-  s.required_ruby_version = Gem::Requirement.new(">= 2.0.0".freeze)
-  s.rubygems_version = "2.5.2.1".freeze
-  s.summary = "Complete geocoding solution for Ruby.".freeze
+  s.name        = "geocoder"
+  s.required_ruby_version = '>= 2.0.0'
+  s.version     = Geocoder::VERSION
+  s.platform    = Gem::Platform::RUBY
+  s.authors     = ["Alex Reisner"]
+  s.email       = ["alex@alexreisner.com"]
+  s.homepage    = "http://www.rubygeocoder.com"
+  s.date        = Date.today.to_s
+  s.summary     = "Complete geocoding solution for Ruby."
+  s.description = "Object geocoding (by street or IP address), reverse geocoding (coordinates to street address), distance queries for ActiveRecord and Mongoid, result caching, and more. Designed for Rails but works with Sinatra and other Rack frameworks too."
+  s.files       = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'examples/**/*', 'lib/**/*', 'bin/*']
+  s.require_paths = ["lib"]
+  s.executables = ["geocode"]
+  s.license     = 'MIT'
+  s.metadata = {
+    'source_code_uri' => 'https://github.com/alexreisner/geocoder',
+    'changelog_uri'   => 'https://github.com/alexreisner/geocoder/blob/master/CHANGELOG.md'
+  }
 end
diff --git a/init.rb b/init.rb
new file mode 100644
index 0000000..87883b6
--- /dev/null
+++ b/init.rb
@@ -0,0 +1 @@
+require 'geocoder'
\ No newline at end of file
diff --git a/lib/easting_northing.rb b/lib/easting_northing.rb
new file mode 100644
index 0000000..0b9d2a9
--- /dev/null
+++ b/lib/easting_northing.rb
@@ -0,0 +1,171 @@
+module Geocoder
+  class EastingNorthing
+    attr_reader :easting, :northing, :lat_lng
+
+    def initialize(opts)
+      @easting = opts[:easting]
+      @northing = opts[:northing]
+
+      @lat_lng = to_WGS84(to_osgb_36)
+    end
+
+    private
+
+    def to_osgb_36
+      osgb_fo  = 0.9996012717
+      northing0 = -100_000.0
+      easting0 = 400_000.0
+      phi0 = deg_to_rad(49.0)
+      lambda0 = deg_to_rad(-2.0)
+      a = 6_377_563.396
+      b = 6_356_256.909
+      eSquared = ((a * a) - (b * b)) / (a * a)
+      phi = 0.0
+      lambda = 0.0
+      n = (a - b) / (a + b)
+      m = 0.0
+      phiPrime = ((northing - northing0) / (a * osgb_fo)) + phi0
+
+      while (northing - northing0 - m) >= 0.001
+        m =
+          (b * osgb_fo)\
+            * (((1 + n + ((5.0 / 4.0) * n * n) + ((5.0 / 4.0) * n * n * n))\
+              * (phiPrime - phi0))\
+              - (((3 * n) + (3 * n * n) + ((21.0 / 8.0) * n * n * n))\
+                * Math.sin(phiPrime - phi0)\
+                * Math.cos(phiPrime + phi0))\
+              + ((((15.0 / 8.0) * n * n) + ((15.0 / 8.0) * n * n * n))\
+                * Math.sin(2.0 * (phiPrime - phi0))\
+                * Math.cos(2.0 * (phiPrime + phi0)))\
+              - (((35.0 / 24.0) * n * n * n)\
+                * Math.sin(3.0 * (phiPrime - phi0))\
+                * Math.cos(3.0 * (phiPrime + phi0))))
+
+        phiPrime += (northing - northing0 - m) / (a * osgb_fo)
+      end
+
+      v = a * osgb_fo * ((1.0 - eSquared * sin_pow_2(phiPrime))**-0.5)
+      rho =
+        a\
+          * osgb_fo\
+          * (1.0 - eSquared)\
+          * ((1.0 - eSquared * sin_pow_2(phiPrime))**-1.5)
+      etaSquared = (v / rho) - 1.0
+      vii = Math.tan(phiPrime) / (2 * rho * v)
+      viii =
+        (Math.tan(phiPrime) / (24.0 * rho * (v**3.0)))\
+          * (5.0\
+            + (3.0 * tan_pow_2(phiPrime))\
+            + etaSquared\
+            - (9.0 * tan_pow_2(phiPrime) * etaSquared))
+      ix =
+        (Math.tan(phiPrime) / (720.0 * rho * (v**5.0)))\
+          * (61.0\
+            + (90.0 * tan_pow_2(phiPrime))\
+            + (45.0 * tan_pow_2(phiPrime) * tan_pow_2(phiPrime)))
+      x = sec(phiPrime) / v
+      xi =
+        (sec(phiPrime) / (6.0 * v * v * v))\
+          * ((v / rho) + (2 * tan_pow_2(phiPrime)))
+      xiii =
+        (sec(phiPrime) / (120.0 * (v**5.0)))\
+          * (5.0\
+            + (28.0 * tan_pow_2(phiPrime))\
+            + (24.0 * tan_pow_2(phiPrime) * tan_pow_2(phiPrime)))
+      xiia =
+        (sec(phiPrime) / (5040.0 * (v**7.0)))\
+          * (61.0\
+            + (662.0 * tan_pow_2(phiPrime))\
+            + (1320.0 * tan_pow_2(phiPrime) * tan_pow_2(phiPrime))\
+            + (720.0\
+              * tan_pow_2(phiPrime)\
+              * tan_pow_2(phiPrime)\
+              * tan_pow_2(phiPrime)))
+      phi =
+        phiPrime\
+          - (vii * ((easting - easting0)**2.0))\
+          + (viii * ((easting - easting0)**4.0))\
+          - (ix * ((easting - easting0)**6.0))
+      lambda =
+        lambda0\
+          + (x * (easting - easting0))\
+          - (xi * ((easting - easting0)**3.0))\
+          + (xiii * ((easting - easting0)**5.0))\
+          - (xiia * ((easting - easting0)**7.0))
+
+      [rad_to_deg(phi), rad_to_deg(lambda)]
+    end
+
+    def to_WGS84(latlng)
+      latitude = latlng[0]
+      longitude = latlng[1]
+
+      a = 6_377_563.396
+      b = 6_356_256.909
+      eSquared = ((a * a) - (b * b)) / (a * a)
+
+      phi = deg_to_rad(latitude)
+      lambda = deg_to_rad(longitude)
+      v = a / Math.sqrt(1 - eSquared * sin_pow_2(phi))
+      h = 0
+      x = (v + h) * Math.cos(phi) * Math.cos(lambda)
+      y = (v + h) * Math.cos(phi) * Math.sin(lambda)
+      z = ((1 - eSquared) * v + h) * Math.sin(phi)
+
+      tx = 446.448
+      ty = -124.157
+      tz = 542.060
+
+      s  = -0.0000204894
+      rx = deg_to_rad(0.00004172222)
+      ry = deg_to_rad(0.00006861111)
+      rz = deg_to_rad(0.00023391666)
+
+      xB = tx + (x * (1 + s)) + (-rx * y) + (ry * z)
+      yB = ty + (rz * x) + (y * (1 + s)) + (-rx * z)
+      zB = tz + (-ry * x) + (rx * y) + (z * (1 + s))
+
+      a = 6_378_137.000
+      b = 6_356_752.3141
+      eSquared = ((a * a) - (b * b)) / (a * a)
+
+      lambdaB = rad_to_deg(Math.atan(yB / xB))
+      p = Math.sqrt((xB * xB) + (yB * yB))
+      phiN = Math.atan(zB / (p * (1 - eSquared)))
+
+      (1..10).each do |_i|
+        v = a / Math.sqrt(1 - eSquared * sin_pow_2(phiN))
+        phiN1 = Math.atan((zB + (eSquared * v * Math.sin(phiN))) / p)
+        phiN = phiN1
+      end
+
+      phiB = rad_to_deg(phiN)
+
+      [phiB, lambdaB]
+    end
+
+    def deg_to_rad(degrees)
+      degrees / 180.0 * Math::PI
+    end
+
+    def rad_to_deg(r)
+      (r / Math::PI) * 180
+    end
+
+    def sin_pow_2(x)
+      Math.sin(x) * Math.sin(x)
+    end
+
+    def cos_pow_2(x)
+      Math.cos(x) * Math.cos(x)
+    end
+
+    def tan_pow_2(x)
+      Math.tan(x) * Math.tan(x)
+    end
+
+    def sec(x)
+      1.0 / Math.cos(x)
+    end
+  end
+end
diff --git a/lib/generators/geocoder/config/templates/initializer.rb b/lib/generators/geocoder/config/templates/initializer.rb
index 0e64173..b653516 100644
--- a/lib/generators/geocoder/config/templates/initializer.rb
+++ b/lib/generators/geocoder/config/templates/initializer.rb
@@ -9,7 +9,6 @@ Geocoder.configure(
   # https_proxy: nil,           # HTTPS proxy server (user:pass@host:port)
   # api_key: nil,               # API key for geocoding service
   # cache: nil,                 # cache object (must respond to #[], #[]=, and #del)
-  # cache_prefix: 'geocoder:',  # prefix (string) to use for all cache keys
 
   # Exceptions that should not be rescued by default
   # (if you want to implement custom error handling);
@@ -19,4 +18,10 @@ Geocoder.configure(
   # Calculation options
   # units: :mi,                 # :km for kilometers or :mi for miles
   # distances: :linear          # :spherical or :linear
+
+  # Cache configuration
+  # cache_options: {
+  #   expiration: 2.days,
+  #   prefix: 'geocoder:'
+  # }
 )
diff --git a/lib/geocoder/cache.rb b/lib/geocoder/cache.rb
index 9eb032a..e4eec67 100644
--- a/lib/geocoder/cache.rb
+++ b/lib/geocoder/cache.rb
@@ -1,37 +1,29 @@
+Dir["#{__dir__}/cache_stores/*.rb"].each {|file| require file }
+
 module Geocoder
   class Cache
 
-    def initialize(store, prefix)
-      @store = store
-      @prefix = prefix
+    def initialize(store, config)
+      @class = (Object.const_get("Geocoder::CacheStore::#{store.class}") rescue Geocoder::CacheStore::Generic)
+      @store_service = @class.new(store, config)
     end
 
     ##
     # Read from the Cache.
     #
     def [](url)
-      interpret case
-        when store.respond_to?(:[])
-          store[key_for(url)]
-        when store.respond_to?(:get)
-          store.get key_for(url)
-        when store.respond_to?(:read)
-          store.read key_for(url)
-      end
+      interpret store_service.read(url)
+    rescue => e
+      Geocoder.log(:warn, "Geocoder cache read error: #{e}")
     end
 
     ##
     # Write to the Cache.
     #
     def []=(url, value)
-      case
-        when store.respond_to?(:[]=)
-          store[key_for(url)] = value
-        when store.respond_to?(:set)
-          store.set key_for(url), value
-        when store.respond_to?(:write)
-          store.write key_for(url), value
-      end
+      store_service.write(url, value)
+    rescue => e
+      Geocoder.log(:warn, "Geocoder cache write error: #{e}")
     end
 
     ##
@@ -40,7 +32,7 @@ module Geocoder
     #
     def expire(url)
       if url == :all
-        if store.respond_to?(:keys)
+        if store_service.respond_to?(:keys)
           urls.each{ |u| expire(u) }
         else
           raise(NoMethodError, "The Geocoder cache store must implement `#keys` for `expire(:all)` to work")
@@ -53,29 +45,21 @@ module Geocoder
 
     private # ----------------------------------------------------------------
 
-    def prefix; @prefix; end
-    def store; @store; end
-
-    ##
-    # Cache key for a given URL.
-    #
-    def key_for(url)
-      [prefix, url].join
-    end
+    def store_service; @store_service; end
 
     ##
     # Array of keys with the currently configured prefix
     # that have non-nil values.
     #
     def keys
-      store.keys.select{ |k| k.match(/^#{prefix}/) and self[k] }
+      store_service.keys
     end
 
     ##
     # Array of cached URLs.
     #
     def urls
-      keys.map{ |k| k[/^#{prefix}(.*)/, 1] }
+      store_service.urls
     end
 
     ##
@@ -87,8 +71,7 @@ module Geocoder
     end
 
     def expire_single_url(url)
-      key = key_for(url)
-      store.respond_to?(:del) ? store.del(key) : store.delete(key)
+      store_service.remove(url)
     end
   end
 end
diff --git a/lib/geocoder/cache_stores/base.rb b/lib/geocoder/cache_stores/base.rb
new file mode 100644
index 0000000..14b253b
--- /dev/null
+++ b/lib/geocoder/cache_stores/base.rb
@@ -0,0 +1,40 @@
+module Geocoder::CacheStore
+  class Base
+    def initialize(store, options)
+      @store = store
+      @config = options
+      @prefix = config[:prefix]
+    end
+
+    ##
+    # Array of keys with the currently configured prefix
+    # that have non-nil values.
+    def keys
+      store.keys.select { |k| k.match(/^#{prefix}/) and self[k] }
+    end
+
+    ##
+    # Array of cached URLs.
+    #
+    def urls
+      keys
+    end
+
+    protected # ----------------------------------------------------------------
+
+    def prefix; @prefix; end
+    def store; @store; end
+    def config; @config; end
+
+    ##
+    # Cache key for a given URL.
+    #
+    def key_for(url)
+      if url.match(/^#{prefix}/)
+        url
+      else
+        [prefix, url].join
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/lib/geocoder/cache_stores/generic.rb b/lib/geocoder/cache_stores/generic.rb
new file mode 100644
index 0000000..791b3cf
--- /dev/null
+++ b/lib/geocoder/cache_stores/generic.rb
@@ -0,0 +1,35 @@
+require 'geocoder/cache_stores/base'
+
+module Geocoder::CacheStore
+  class Generic < Base
+    def write(url, value)
+      case
+      when store.respond_to?(:[]=)
+        store[key_for(url)] = value
+      when store.respond_to?(:set)
+        store.set key_for(url), value
+      when store.respond_to?(:write)
+        store.write key_for(url), value
+      end
+    end
+
+    def read(url)
+      case
+      when store.respond_to?(:[])
+        store[key_for(url)]
+      when store.respond_to?(:get)
+        store.get key_for(url)
+      when store.respond_to?(:read)
+        store.read key_for(url)
+      end
+    end
+
+    def keys
+      store.keys
+    end
+
+    def remove(key)
+      store.delete(key)
+    end
+  end
+end
diff --git a/lib/geocoder/cache_stores/redis.rb b/lib/geocoder/cache_stores/redis.rb
new file mode 100644
index 0000000..39c3798
--- /dev/null
+++ b/lib/geocoder/cache_stores/redis.rb
@@ -0,0 +1,34 @@
+require 'geocoder/cache_stores/base'
+
+module Geocoder::CacheStore
+  class Redis < Base
+    def initialize(store, options)
+      super
+      @expiration = options[:expiration]
+    end
+
+    def write(url, value, expire = @expiration)
+      if expire.present?
+        store.set key_for(url), value, ex: expire
+      else
+        store.set key_for(url), value
+      end
+    end
+
+    def read(url)
+      store.get key_for(url)
+    end
+
+    def keys
+      store.keys("#{prefix}*")
+    end
+
+    def remove(key)
+      store.del(key)
+    end
+
+    private # ----------------------------------------------------------------
+
+    def expire; @expiration; end
+  end
+end
diff --git a/lib/geocoder/configuration.rb b/lib/geocoder/configuration.rb
index 8e097a2..d352200 100644
--- a/lib/geocoder/configuration.rb
+++ b/lib/geocoder/configuration.rb
@@ -1,5 +1,6 @@
 require 'singleton'
 require 'geocoder/configuration_hash'
+require 'geocoder/util'
 
 module Geocoder
 
@@ -54,19 +55,20 @@ module Geocoder
       :lookup,
       :ip_lookup,
       :language,
+      :host,
       :http_headers,
       :use_https,
       :http_proxy,
       :https_proxy,
       :api_key,
       :cache,
-      :cache_prefix,
       :always_raise,
       :units,
       :distances,
       :basic_auth,
       :logger,
-      :kernel_logger_level
+      :kernel_logger_level,
+      :cache_options
     ]
 
     attr_accessor :data
@@ -75,6 +77,10 @@ module Geocoder
       instance.set_defaults
     end
 
+    def self.initialize
+      instance.send(:initialize)
+    end
+
     OPTIONS.each do |o|
       define_method o do
         @data[o]
@@ -85,7 +91,7 @@ module Geocoder
     end
 
     def configure(options)
-      @data.rmerge!(options)
+      Util.recursive_hash_merge(@data, options)
     end
 
     def initialize # :nodoc
@@ -105,8 +111,6 @@ module Geocoder
       @data[:http_proxy]   = nil         # HTTP proxy server (user:pass@host:port)
       @data[:https_proxy]  = nil         # HTTPS proxy server (user:pass@host:port)
       @data[:api_key]      = nil         # API key for geocoding service
-      @data[:cache]        = nil         # cache object (must respond to #[], #[]=, and #keys)
-      @data[:cache_prefix] = "geocoder:" # prefix (string) to use for all cache keys
       @data[:basic_auth]   = {}          # user and password for basic auth ({:user => "user", :password => "password"})
       @data[:logger]       = :kernel     # :kernel or Logger instance
       @data[:kernel_logger_level] = ::Logger::WARN # log level, if kernel logger is used
@@ -119,6 +123,16 @@ module Geocoder
       # calculation options
       @data[:units]     = :mi      # :mi or :km
       @data[:distances] = :linear  # :linear or :spherical
+
+      # Set the default values for the caching mechanism
+      # By default, the cache keys will not expire as IP addresses and phyiscal
+      # addresses will rarely change.
+      @data[:cache]        = nil   # cache object (must respond to #[], #[]=, and optionally #keys)
+      @data[:cache_prefix] = nil   # - DEPRECATED - prefix (string) to use for all cache keys
+      @data[:cache_options] = {
+        prefix: 'geocoder:',
+        expiration: nil
+      }
     end
 
     instance_eval(OPTIONS.map do |option|
diff --git a/lib/geocoder/configuration_hash.rb b/lib/geocoder/configuration_hash.rb
index 70569c3..19d21ba 100644
--- a/lib/geocoder/configuration_hash.rb
+++ b/lib/geocoder/configuration_hash.rb
@@ -1,11 +1,11 @@
-require 'hash_recursive_merge'
-
 module Geocoder
   class ConfigurationHash < Hash
-    include HashRecursiveMerge
-
     def method_missing(meth, *args, &block)
       has_key?(meth) ? self[meth] : super
     end
+
+    def respond_to_missing?(meth, include_private = false)
+      has_key?(meth) || super
+    end
   end
 end
diff --git a/lib/geocoder/ip_address.rb b/lib/geocoder/ip_address.rb
index c6858cc..c3c39ac 100644
--- a/lib/geocoder/ip_address.rb
+++ b/lib/geocoder/ip_address.rb
@@ -7,6 +7,15 @@ module Geocoder
       '192.168.0.0/16',
     ].map { |ip| IPAddr.new(ip) }.freeze
 
+    def initialize(ip)
+      ip = ip.to_string if ip.is_a?(IPAddr)
+      if ip.is_a?(Hash)
+        super(**ip)
+      else
+        super(ip)
+      end
+    end
+
     def internal?
       loopback? || private?
     end
@@ -20,7 +29,8 @@ module Geocoder
     end
 
     def valid?
-      !!((self =~ Resolv::IPv4::Regex) || (self =~ Resolv::IPv6::Regex))
+      ip = self[/(?<=\[)(.*?)(?=\])/] || self
+      !!((ip =~ Resolv::IPv4::Regex) || (ip =~ Resolv::IPv6::Regex))
     end
   end
 end
diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb
index 485e28c..7c23dab 100644
--- a/lib/geocoder/lookup.rb
+++ b/lib/geocoder/lookup.rb
@@ -18,6 +18,14 @@ module Geocoder
       all_services - [:test]
     end
 
+    ##
+    # Array of valid Lookup service names, excluding any that do not build their own HTTP requests.
+    # For example, Amazon Location Service uses the AWS gem, not HTTP REST requests, to fetch data.
+    #
+    def all_services_with_http_requests
+      all_services_except_test - [:amazon_location_service]
+    end
+
     ##
     # All street address lookup services, default first.
     #
@@ -32,11 +40,12 @@ module Geocoder
         :google_places_search,
         :bing,
         :geocoder_ca,
-        :geocoder_us,
         :yandex,
+        :nationaal_georegister_nl,
         :nominatim,
         :mapbox,
         :mapquest,
+        :uk_ordnance_survey_names,
         :opencagedata,
         :pelias,
         :pickpoint,
@@ -51,7 +60,13 @@ module Geocoder
         :ban_data_gouv_fr,
         :test,
         :latlon,
-        :amap
+        :amap,
+        :osmnames,
+        :melissa_street,
+        :amazon_location_service,
+        :geoapify,
+        :photon,
+        :twogis
       ]
     end
 
@@ -61,6 +76,7 @@ module Geocoder
     def ip_services
       @ip_services ||= [
         :baidu_ip,
+        :abstract_api,
         :freegeoip,
         :geoip2,
         :maxmind,
@@ -69,11 +85,15 @@ module Geocoder
         :pointpin,
         :maxmind_geoip2,
         :ipinfo_io,
+        :ipregistry,
         :ipapi_com,
         :ipdata_co,
         :db_ip_com,
         :ipstack,
-        :ip2location
+        :ip2location,
+        :ipgeolocation,
+        :ipqualityscore,
+        :ipbase
       ]
     end
 
@@ -99,8 +119,7 @@ module Geocoder
     def spawn(name)
       if all_services.include?(name)
         name = name.to_s
-        require "geocoder/lookups/#{name}"
-        Geocoder::Lookup.const_get(classify_name(name)).new
+        instantiate_lookup(name)
       else
         valids = all_services.map(&:inspect).join(", ")
         raise ConfigurationError, "Please specify a valid lookup for Geocoder " +
@@ -114,5 +133,18 @@ module Geocoder
     def classify_name(filename)
       filename.to_s.split("_").map{ |i| i[0...1].upcase + i[1..-1] }.join
     end
+
+    ##
+    # Safely instantiate Lookup
+    #
+    def instantiate_lookup(name)
+      class_name = classify_name(name)
+      begin
+        Geocoder::Lookup.const_get(class_name, inherit=false)
+      rescue NameError
+        require "geocoder/lookups/#{name}"
+      end
+      Geocoder::Lookup.const_get(class_name).new
+    end
   end
 end
diff --git a/lib/geocoder/lookups/abstract_api.rb b/lib/geocoder/lookups/abstract_api.rb
new file mode 100644
index 0000000..f75baeb
--- /dev/null
+++ b/lib/geocoder/lookups/abstract_api.rb
@@ -0,0 +1,46 @@
+# encoding: utf-8
+
+require 'geocoder/lookups/base'
+require 'geocoder/results/abstract_api'
+
+module Geocoder::Lookup
+  class AbstractApi < Base
+
+    def name
+      "Abstract API"
+    end
+
+    def required_api_key_parts
+      ['api_key']
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    private # ---------------------------------------------------------------
+
+    def base_query_url(query)
+      "#{protocol}://ipgeolocation.abstractapi.com/v1/?"
+    end
+
+    def query_url_params(query)
+      params = {api_key: configuration.api_key}
+
+      ip_address = query.sanitized_text
+      if ip_address.is_a?(String) && ip_address.length > 0
+        params[:ip_address] = ip_address
+      end
+
+      params.merge(super)
+    end
+
+    def results(query, reverse = false)
+      if doc = fetch_data(query)
+        [doc]
+      else
+        []
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/amap.rb b/lib/geocoder/lookups/amap.rb
index ff2ac18..57d8d4a 100644
--- a/lib/geocoder/lookups/amap.rb
+++ b/lib/geocoder/lookups/amap.rb
@@ -32,10 +32,10 @@ module Geocoder::Lookup
         return doc['geocodes'] unless doc['geocodes'].blank?
       when ['0', 'INVALID_USER_KEY']
         raise_error(Geocoder::InvalidApiKey, "invalid api key") ||
-          warn("#{self.name} Geocoding API error: invalid api key.")
+          Geocoder.log(:warn, "#{self.name} Geocoding API error: invalid api key.")
       else
         raise_error(Geocoder::Error, "server error.") ||
-          warn("#{self.name} Geocoding API error: server error - [#{doc['info']}]")
+          Geocoder.log(:warn, "#{self.name} Geocoding API error: server error - [#{doc['info']}]")
       end
       return []
     end
diff --git a/lib/geocoder/lookups/amazon_location_service.rb b/lib/geocoder/lookups/amazon_location_service.rb
new file mode 100644
index 0000000..a60e51c
--- /dev/null
+++ b/lib/geocoder/lookups/amazon_location_service.rb
@@ -0,0 +1,54 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/amazon_location_service'
+
+module Geocoder::Lookup
+  class AmazonLocationService < Base
+    def results(query)
+      params = query.options.dup
+
+      # index_name is required
+      # Aws::ParamValidator raises ArgumentError on missing required keys
+      params.merge!(index_name: configuration[:index_name])
+
+      # Aws::ParamValidator raises ArgumentError on unexpected keys
+      params.delete(:lookup) 
+      
+      resp = if query.reverse_geocode?
+        client.search_place_index_for_position(params.merge(position: query.coordinates.reverse))
+      else
+        client.search_place_index_for_text(params.merge(text: query.text))
+      end
+      
+      resp.results.map(&:place)
+    end
+
+    private
+
+    def client
+      return @client if @client
+      require_sdk
+      keys = configuration.api_key
+      if keys
+        @client = Aws::LocationService::Client.new(
+          access_key_id: keys[:access_key_id],
+          secret_access_key: keys[:secret_access_key],
+        )
+      else
+        @client = Aws::LocationService::Client.new
+      end
+    end
+
+    def require_sdk
+      begin
+        require 'aws-sdk-locationservice'
+      rescue LoadError
+        raise_error(Geocoder::ConfigurationError) ||
+          Geocoder.log(
+            :error,
+            "Couldn't load the Amazon Location Service SDK. " +
+            "Install it with: gem install aws-sdk-locationservice -v '~> 1.4'"
+          )
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/ban_data_gouv_fr.rb b/lib/geocoder/lookups/ban_data_gouv_fr.rb
index b4b2e81..56f0e87 100644
--- a/lib/geocoder/lookups/ban_data_gouv_fr.rb
+++ b/lib/geocoder/lookups/ban_data_gouv_fr.rb
@@ -22,7 +22,7 @@ module Geocoder::Lookup
     end
 
     def any_result?(doc)
-      doc['features'].any?
+      doc['features'] and doc['features'].any?
     end
 
     def results(query)
@@ -86,6 +86,12 @@ module Geocoder::Lookup
       unless (citycode = query.options[:citycode]).nil? || !code_param_is_valid?(citycode)
         params[:citycode] = citycode.to_s
       end
+      unless (lat = query.options[:lat]).nil? || !latitude_is_valid?(lat)
+        params[:lat] = lat
+      end
+      unless (lon = query.options[:lon]).nil? || !longitude_is_valid?(lon)
+        params[:lon] = lon
+      end
       params
     end
 
@@ -126,5 +132,12 @@ module Geocoder::Lookup
       (1..99999).include?(param.to_i)
     end
 
+    def latitude_is_valid?(param)
+      param.to_f <= 90 && param.to_f >= -90
+    end
+
+    def longitude_is_valid?(param)
+      param.to_f <= 180 && param.to_f >= -180
+    end
   end
 end
diff --git a/lib/geocoder/lookups/base.rb b/lib/geocoder/lookups/base.rb
index 8169960..f0c1137 100644
--- a/lib/geocoder/lookups/base.rb
+++ b/lib/geocoder/lookups/base.rb
@@ -84,7 +84,8 @@ module Geocoder
       #
       def cache
         if @cache.nil? and store = configuration.cache
-          @cache = Cache.new(store, configuration.cache_prefix)
+          cache_options = configuration.cache_options
+          @cache = Cache.new(store, cache_options)
         end
         @cache
       end
@@ -197,6 +198,8 @@ module Geocoder
         raise_error(err) or Geocoder.log(:warn, "Geocoding API connection cannot be established.")
       rescue Errno::ECONNREFUSED => err
         raise_error(err) or Geocoder.log(:warn, "Geocoding API connection refused.")
+      rescue Geocoder::NetworkError => err
+        raise_error(err) or Geocoder.log(:warn, "Geocoding API connection is either unreacheable or reset by the peer")
       rescue Timeout::Error => err
         raise_error(err) or Geocoder.log(:warn, "Geocoding API not responding fast enough " +
           "(use Geocoder.configure(:timeout => ...) to set limit).")
diff --git a/lib/geocoder/lookups/bing.rb b/lib/geocoder/lookups/bing.rb
index c2d0edc..7ab4945 100644
--- a/lib/geocoder/lookups/bing.rb
+++ b/lib/geocoder/lookups/bing.rb
@@ -54,7 +54,7 @@ module Geocoder::Lookup
     def query_url_params(query)
       {
         key: configuration.api_key,
-        language: (query.language || configuration.language)
+        culture: (query.language || configuration.language)
       }.merge(super)
     end
 
diff --git a/lib/geocoder/lookups/esri.rb b/lib/geocoder/lookups/esri.rb
index 6f6cf0e..c12deb0 100644
--- a/lib/geocoder/lookups/esri.rb
+++ b/lib/geocoder/lookups/esri.rb
@@ -9,6 +9,10 @@ module Geocoder::Lookup
       "Esri"
     end
 
+      def supported_protocols
+        [:https]
+      end
+
     private # ---------------------------------------------------------------
 
     def base_query_url(query)
@@ -47,6 +51,8 @@ module Geocoder::Lookup
         params[:forStorage] = for_storage_value
       end
       params[:sourceCountry] = configuration[:source_country] if configuration[:source_country]
+      params[:preferredLabelValues] = configuration[:preferred_label_values] if configuration[:preferred_label_values]
+
       params.merge(super)
     end
 
diff --git a/lib/geocoder/lookups/freegeoip.rb b/lib/geocoder/lookups/freegeoip.rb
index 6b551a2..b712949 100644
--- a/lib/geocoder/lookups/freegeoip.rb
+++ b/lib/geocoder/lookups/freegeoip.rb
@@ -10,21 +10,23 @@ module Geocoder::Lookup
 
     def supported_protocols
       if configuration[:host]
-        [:http, :https]
+        [:https]
       else
         # use https for default host
         [:https]
       end
     end
 
-    def query_url(query)
-      "#{protocol}://#{host}/json/#{query.sanitized_text}"
-    end
-
     private # ---------------------------------------------------------------
 
-    def cache_key(query)
-      query_url(query)
+    def base_query_url(query)
+      "#{protocol}://#{host}/json/#{query.sanitized_text}?"
+    end
+
+    def query_url_params(query)
+      {
+        :apikey => configuration.api_key
+      }.merge(super)
     end
 
     def parse_raw_data(raw_data)
@@ -44,8 +46,8 @@ module Geocoder::Lookup
         "city"         => "",
         "region_code"  => "",
         "region_name"  => "",
-        "metrocode"    => "",
-        "zipcode"      => "",
+        "metro_code"    => "",
+        "zip_code"      => "",
         "latitude"     => "0",
         "longitude"    => "0",
         "country_name" => "Reserved",
@@ -54,7 +56,7 @@ module Geocoder::Lookup
     end
 
     def host
-      configuration[:host] || "freegeoip.net"
+      configuration[:host] || "freegeoip.app"
     end
   end
 end
diff --git a/lib/geocoder/lookups/geoapify.rb b/lib/geocoder/lookups/geoapify.rb
new file mode 100644
index 0000000..c9d74e7
--- /dev/null
+++ b/lib/geocoder/lookups/geoapify.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'geocoder/lookups/base'
+require 'geocoder/results/geoapify'
+
+module Geocoder
+  module Lookup
+    # https://apidocs.geoapify.com/docs/geocoding/api
+    class Geoapify < Base
+      def name
+        'Geoapify'
+      end
+
+      def required_api_key_parts
+        ['api_key']
+      end
+
+      def supported_protocols
+        [:https]
+      end
+
+      private
+
+      def base_query_url(query)
+        method = if query.reverse_geocode?
+          'reverse'
+        elsif query.options[:autocomplete]
+          'autocomplete'
+        else
+          'search'
+        end
+        "https://api.geoapify.com/v1/geocode/#{method}?"
+      end
+
+      def results(query)
+        return [] unless (doc = fetch_data(query))
+
+        # The rest of the status codes should be already handled by the default
+        # functionality as the API returns correct HTTP response codes in most
+        # cases. There may be some unhandled cases still (such as over query
+        # limit reached) but there is not enough documentation to cover them.
+        case doc['statusCode']
+        when 500
+          raise_error(Geocoder::InvalidRequest) || Geocoder.log(:warn, doc['message'])
+        end
+
+        return [] unless doc['type'] == 'FeatureCollection'
+        return [] unless doc['features'] || doc['features'].present?
+
+        doc['features']
+      end
+
+      def query_url_params(query)
+        lang = query.language || configuration.language
+        params = { apiKey: configuration.api_key, lang: lang, limit: query.options[:limit] }
+
+        if query.reverse_geocode?
+          params.merge!(query_url_params_reverse(query))
+        else
+          params.merge!(query_url_params_coordinates(query))
+        end
+
+        params.merge!(super)
+      end
+
+      def query_url_params_coordinates(query)
+        { text: query.sanitized_text }
+      end
+
+      def query_url_params_reverse(query)
+        {
+          lat: query.coordinates[0],
+          lon: query.coordinates[1]
+        }
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/geocoder_us.rb b/lib/geocoder/lookups/geocoder_us.rb
deleted file mode 100644
index cc9869c..0000000
--- a/lib/geocoder/lookups/geocoder_us.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'geocoder/lookups/base'
-require "geocoder/results/geocoder_us"
-
-module Geocoder::Lookup
-  class GeocoderUs < Base
-
-    def name
-      "Geocoder.us"
-    end
-
-    def supported_protocols
-      [:http]
-    end
-
-    private # ----------------------------------------------------------------
-
-    def base_query_url(query)
-      base_query_url_with_optional_key(configuration.api_key)
-    end
-
-    def cache_key(query)
-      base_query_url_with_optional_key(nil) + url_query_string(query)
-    end
-
-    def base_query_url_with_optional_key(key = nil)
-      base = "#{protocol}://"
-      if configuration.api_key
-        base << "#{configuration.api_key}@"
-      end
-      base + "geocoder.us/member/service/csv/geocode?"
-    end
-
-    def results(query)
-      return [] unless doc = fetch_data(query)
-      if doc[0].to_s =~ /^(\d+)\:/
-        return []
-      else
-        return [doc.size == 5 ? ((doc[0..1] << nil) + doc[2..4]) : doc]
-      end
-    end
-
-    def query_url_params(query)
-      (query.text =~ /^\d{5}(?:-\d{4})?$/ ? {:zip => query} : {:address => query.sanitized_text}).merge(super)
-    end
-
-    def parse_raw_data(raw_data)
-      raw_data.chomp.split(',')
-    end
-  end
-end
-
diff --git a/lib/geocoder/lookups/geocodio.rb b/lib/geocoder/lookups/geocodio.rb
index 14e2655..58a0629 100644
--- a/lib/geocoder/lookups/geocodio.rb
+++ b/lib/geocoder/lookups/geocodio.rb
@@ -29,7 +29,7 @@ module Geocoder::Lookup
 
     def base_query_url(query)
       path = query.reverse_geocode? ? "reverse" : "geocode"
-      "#{protocol}://api.geocod.io/v1.3/#{path}?"
+      "#{protocol}://api.geocod.io/v1.6/#{path}?"
     end
 
     def query_url_params(query)
diff --git a/lib/geocoder/lookups/geoip2.rb b/lib/geocoder/lookups/geoip2.rb
index de647ad..f198b25 100644
--- a/lib/geocoder/lookups/geoip2.rb
+++ b/lib/geocoder/lookups/geoip2.rb
@@ -37,6 +37,10 @@ module Geocoder
       def results(query)
         return [] unless configuration[:file]
 
+        if @mmdb.respond_to?(:local_ip_alias) && !configuration[:local_ip_alias].nil?
+          @mmdb.local_ip_alias = configuration[:local_ip_alias]
+        end
+
         result = @mmdb.lookup(query.to_s)
         result.nil? ? [] : [result]
       end
diff --git a/lib/geocoder/lookups/geoportail_lu.rb b/lib/geocoder/lookups/geoportail_lu.rb
index bb35a0e..6c4154a 100644
--- a/lib/geocoder/lookups/geoportail_lu.rb
+++ b/lib/geocoder/lookups/geoportail_lu.rb
@@ -56,7 +56,7 @@ module Geocoder
         else
           result = []
           raise_error(Geocoder::Error) ||
-              warn("Geportail.lu Geocoding API error")
+              Geocoder.log(:warn, "Geportail.lu Geocoding API error")
         end
         result
       end
diff --git a/lib/geocoder/lookups/google.rb b/lib/geocoder/lookups/google.rb
index b209f8d..81df164 100644
--- a/lib/geocoder/lookups/google.rb
+++ b/lib/geocoder/lookups/google.rb
@@ -44,10 +44,15 @@ module Geocoder::Lookup
       super(response) and ['OK', 'ZERO_RESULTS'].include?(status)
     end
 
+    def result_root_attr
+      'results'
+    end
+
     def results(query)
       return [] unless doc = fetch_data(query)
-      case doc['status']; when "OK" # OK status implies >0 results
-        return doc['results']
+      case doc['status']
+      when "OK" # OK status implies >0 results
+        return doc[result_root_attr]
       when "OVER_QUERY_LIMIT"
         raise_error(Geocoder::OverQueryLimitError) ||
           Geocoder.log(:warn, "#{name} API error: over query limit.")
diff --git a/lib/geocoder/lookups/google_places_details.rb b/lib/geocoder/lookups/google_places_details.rb
index 5bc1b17..c96a3c0 100644
--- a/lib/geocoder/lookups/google_places_details.rb
+++ b/lib/geocoder/lookups/google_places_details.rb
@@ -22,26 +22,40 @@ module Geocoder
         "#{protocol}://maps.googleapis.com/maps/api/place/details/json?"
       end
 
+      def result_root_attr
+        'result'
+      end
+
       def results(query)
-        return [] unless doc = fetch_data(query)
-
-        case doc["status"]
-        when "OK"
-          return [doc["result"]]
-        when "OVER_QUERY_LIMIT"
-          raise_error(Geocoder::OverQueryLimitError) || Geocoder.log(:warn, "Google Places Details API error: over query limit.")
-        when "REQUEST_DENIED"
-          raise_error(Geocoder::RequestDenied) || Geocoder.log(:warn, "Google Places Details API error: request denied.")
-        when "INVALID_REQUEST"
-          raise_error(Geocoder::InvalidRequest) || Geocoder.log(:warn, "Google Places Details API error: invalid request.")
+        result = super(query)
+        return [result] unless result.is_a? Array
+
+        result
+      end
+
+      def fields(query)
+        if query.options.has_key?(:fields)
+          return format_fields(query.options[:fields])
         end
 
-        []
+        if configuration.has_key?(:fields)
+          return format_fields(configuration[:fields])
+        end
+
+        nil  # use Google Places defaults
+      end
+
+      def format_fields(*fields)
+        flattened = fields.flatten.compact
+        return if flattened.empty?
+
+        flattened.join(',')
       end
 
       def query_url_google_params(query)
         {
           placeid: query.text,
+          fields: fields(query),
           language: query.language || configuration.language
         }
       end
diff --git a/lib/geocoder/lookups/google_places_search.rb b/lib/geocoder/lookups/google_places_search.rb
index 2461033..8b4e0ee 100644
--- a/lib/geocoder/lookups/google_places_search.rb
+++ b/lib/geocoder/lookups/google_places_search.rb
@@ -18,16 +18,59 @@ module Geocoder
 
       private
 
+      def result_root_attr
+        'candidates'
+      end
+
       def base_query_url(query)
-        "#{protocol}://maps.googleapis.com/maps/api/place/textsearch/json?"
+        "#{protocol}://maps.googleapis.com/maps/api/place/findplacefromtext/json?"
       end
 
       def query_url_google_params(query)
         {
-          query: query.text,
+          input: query.text,
+          inputtype: 'textquery',
+          fields: fields(query),
+          locationbias: locationbias(query),
           language: query.language || configuration.language
         }
       end
+
+      def fields(query)
+        if query.options.has_key?(:fields)
+          return format_fields(query.options[:fields])
+        end
+
+        if configuration.has_key?(:fields)
+          return format_fields(configuration[:fields])
+        end
+
+        default_fields
+      end
+
+      def default_fields
+        legacy = %w[id reference]
+        basic = %w[business_status formatted_address geometry icon name 
+          photos place_id plus_code types]
+        contact = %w[opening_hours]
+        atmosphere = %W[price_level rating user_ratings_total]
+        format_fields(legacy, basic, contact, atmosphere)
+      end
+
+      def format_fields(*fields)
+        flattened = fields.flatten.compact
+        return if flattened.empty?
+
+        flattened.join(',')
+      end
+
+      def locationbias(query)
+        if query.options.has_key?(:locationbias)
+          query.options[:locationbias]
+        else
+          configuration[:locationbias]
+        end
+      end
     end
   end
 end
diff --git a/lib/geocoder/lookups/google_premier.rb b/lib/geocoder/lookups/google_premier.rb
index c985bd8..d2e9642 100644
--- a/lib/geocoder/lookups/google_premier.rb
+++ b/lib/geocoder/lookups/google_premier.rb
@@ -21,6 +21,10 @@ module Geocoder::Lookup
 
     private # ---------------------------------------------------------------
 
+    def result_root_attr
+      'results'
+    end
+
     def cache_key(query)
       "#{protocol}://maps.googleapis.com/maps/api/geocode/json?" + hash_to_query(cache_key_params(query))
     end
diff --git a/lib/geocoder/lookups/here.rb b/lib/geocoder/lookups/here.rb
index e302ef6..0a830c5 100644
--- a/lib/geocoder/lookups/here.rb
+++ b/lib/geocoder/lookups/here.rb
@@ -9,69 +9,65 @@ module Geocoder::Lookup
     end
 
     def required_api_key_parts
-      ["app_id", "app_code"]
+      ['api_key']
+    end
+
+    def supported_protocols
+      [:https]
     end
 
     private # ---------------------------------------------------------------
 
     def base_query_url(query)
-      "#{protocol}://#{if query.reverse_geocode? then 'reverse.' end}geocoder.api.here.com/6.2/#{if query.reverse_geocode? then 'reverse' end}geocode.json?"
+      service = query.reverse_geocode? ? "revgeocode" : "geocode"
+
+      "#{protocol}://#{service}.search.hereapi.com/v1/#{service}?"
     end
 
     def results(query)
-      return [] unless doc = fetch_data(query)
-      return [] unless doc['Response'] && doc['Response']['View']
-      if r=doc['Response']['View']
-        return [] if r.nil? || !r.is_a?(Array) || r.empty?
-        return r.first['Result']
+      unless configuration.api_key.is_a?(String)
+        api_key_not_string!
+        return []
       end
-      []
+      return [] unless doc = fetch_data(query)
+      return [] if doc["items"].nil?
+
+      doc["items"]
     end
 
     def query_url_here_options(query, reverse_geocode)
       options = {
-        gen: 9,
-        app_id: api_key,
-        app_code: api_code,
-        language: (query.language || configuration.language)
+        apiKey: configuration.api_key,
+        lang: (query.language || configuration.language)
       }
-      if reverse_geocode
-        options[:mode] = :retrieveAddresses
-        return options
-      end
+      return options if reverse_geocode
 
       unless (country = query.options[:country]).nil?
-        options[:country] = country
+        options[:in] = "countryCode:#{country}"
       end
 
-      unless (mapview = query.options[:bounds]).nil?
-        options[:mapview] = mapview.map{ |point| "%f,%f" % point }.join(';')
-      end
       options
     end
 
     def query_url_params(query)
       if query.reverse_geocode?
         super.merge(query_url_here_options(query, true)).merge(
-          prox: query.sanitized_text
+          at: query.sanitized_text
         )
       else
         super.merge(query_url_here_options(query, false)).merge(
-          searchtext: query.sanitized_text
+          q: query.sanitized_text
         )
       end
     end
 
-    def api_key
-      if (a = configuration.api_key)
-        return a.first if a.is_a?(Array)
-      end
-    end
+    def api_key_not_string!
+      msg = <<~MSG
+        API key for HERE Geocoding and Search API should be a string.
+        For more info on how to obtain it, please see https://developer.here.com/documentation/identity-access-management/dev_guide/topics/plat-using-apikeys.html
+      MSG
 
-    def api_code
-      if (a = configuration.api_key)
-        return a.last if a.is_a?(Array)
-      end
+      raise_error(Geocoder::ConfigurationError, msg) || Geocoder.log(:warn, msg)
     end
   end
 end
diff --git a/lib/geocoder/lookups/ip2location.rb b/lib/geocoder/lookups/ip2location.rb
index 3e29a79..82de1e0 100644
--- a/lib/geocoder/lookups/ip2location.rb
+++ b/lib/geocoder/lookups/ip2location.rb
@@ -8,6 +8,10 @@ module Geocoder::Lookup
       "IP2LocationApi"
     end
 
+    def required_api_key_parts
+      ['key']
+    end
+
     def supported_protocols
       [:http, :https]
     end
@@ -15,15 +19,15 @@ module Geocoder::Lookup
     private # ----------------------------------------------------------------
 
     def base_query_url(query)
-      "#{protocol}://api.ip2location.com/?"
+      "#{protocol}://api.ip2location.com/v2/?"
     end
 
     def query_url_params(query)
-      {
-        key: configuration.api_key ? configuration.api_key : "demo",
-        format: "json",
-        ip: query.sanitized_text
-      }.merge(super)
+      super.merge(
+        key: configuration.api_key,
+        ip: query.sanitized_text,
+        package: configuration[:package],
+      )
     end
 
     def results(query)
@@ -63,13 +67,5 @@ module Geocoder::Lookup
       }
     end
 
-    def query_url_params(query)
-      params = super
-      if configuration.has_key?(:package)
-        params.merge!(package: configuration[:package])
-      end
-      params
-    end
-
   end
 end
diff --git a/lib/geocoder/lookups/ipbase.rb b/lib/geocoder/lookups/ipbase.rb
new file mode 100644
index 0000000..a6cfd95
--- /dev/null
+++ b/lib/geocoder/lookups/ipbase.rb
@@ -0,0 +1,49 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/ipbase'
+
+module Geocoder::Lookup
+  class Ipbase < Base
+
+    def name
+      "ipbase.com"
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    private # ---------------------------------------------------------------
+
+    def base_query_url(query)
+      "https://api.ipbase.com/v2/info?"
+    end
+
+    def query_url_params(query)
+      {
+        :ip => query.sanitized_text,
+        :apikey => configuration.api_key
+      }
+    end
+
+    def results(query)
+      # don't look up a loopback or private address, just return the stored result
+      return [reserved_result(query.text)] if query.internal_ip_address?
+      doc = fetch_data(query) || {}
+      doc.fetch("data", {})["location"] ? [doc] : []
+    end
+
+    def reserved_result(ip)
+      {
+        "data" => {
+          "ip" => ip,
+          "location" => {
+            "city" => { "name" => "" },
+            "country" => { "alpha2" => "RD", "name" => "Reserved" },
+            "region" => { "alpha2" => "", "name" => "" },
+            "zip" => ""
+          }
+        }
+      }
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/ipdata_co.rb b/lib/geocoder/lookups/ipdata_co.rb
index d061696..de11b5a 100644
--- a/lib/geocoder/lookups/ipdata_co.rb
+++ b/lib/geocoder/lookups/ipdata_co.rb
@@ -47,7 +47,7 @@ module Geocoder::Lookup
     end
 
     def host
-      "api.ipdata.co"
+      configuration[:host] || "api.ipdata.co"
     end
 
     def check_response_for_errors!(response)
diff --git a/lib/geocoder/lookups/ipgeolocation.rb b/lib/geocoder/lookups/ipgeolocation.rb
new file mode 100644
index 0000000..adfc655
--- /dev/null
+++ b/lib/geocoder/lookups/ipgeolocation.rb
@@ -0,0 +1,51 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/ipgeolocation'
+
+
+module Geocoder::Lookup
+  class Ipgeolocation < Base
+
+    ERROR_CODES = {
+      400 => Geocoder::RequestDenied, # subscription is paused
+      401 => Geocoder::InvalidApiKey, # missing/invalid API key
+      403 => Geocoder::InvalidRequest, # invalid IP address
+      404 => Geocoder::InvalidRequest, # not found
+      423 => Geocoder::InvalidRequest # bogon/reserved IP address
+    }
+    ERROR_CODES.default = Geocoder::Error
+
+    def name
+      "Ipgeolocation"
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    private # ----------------------------------------------------------------
+
+    def base_query_url(query)
+      "#{protocol}://api.ipgeolocation.io/ipgeo?"
+    end
+    def query_url_params(query)
+      {
+          ip: query.sanitized_text,
+          apiKey: configuration.api_key
+      }.merge(super)
+    end
+
+    def results(query)
+      # don't look up a loopback or private address, just return the stored result
+      return [reserved_result(query.text)] if query.internal_ip_address?
+      [fetch_data(query)]
+    end
+
+    def reserved_result(ip)
+      {
+          "ip"           => ip,
+          "country_name" => "Reserved",
+          "country_code2" => "RD"
+      }
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/ipqualityscore.rb b/lib/geocoder/lookups/ipqualityscore.rb
new file mode 100644
index 0000000..134b4d4
--- /dev/null
+++ b/lib/geocoder/lookups/ipqualityscore.rb
@@ -0,0 +1,50 @@
+# encoding: utf-8
+
+require 'geocoder/lookups/base'
+require 'geocoder/results/ipqualityscore'
+
+module Geocoder::Lookup
+  class Ipqualityscore < Base
+
+    def name
+      "IPQualityScore"
+    end
+
+    def required_api_key_parts
+      ['api_key']
+    end
+
+    private # ---------------------------------------------------------------
+
+    def base_query_url(query)
+      "#{protocol}://ipqualityscore.com/api/json/ip/#{configuration.api_key}/#{query.sanitized_text}?"
+    end
+
+    def valid_response?(response)
+      if (json = parse_json(response.body))
+        success = json['success']
+      end
+      super && success == true
+    end
+
+    def results(query, reverse = false)
+      return [] unless doc = fetch_data(query)
+
+      return [doc] if doc['success']
+
+      case doc['message']
+      when /invalid (.*) key/i
+        raise_error Geocoder::InvalidApiKey ||
+                    Geocoder.log(:warn, "#{name} API error: invalid api key.")
+      when /insufficient credits/, /exceeded your request quota/
+        raise_error Geocoder::OverQueryLimitError ||
+                    Geocoder.log(:warn, "#{name} API error: query limit exceeded.")
+      when /invalid (.*) address/i
+        raise_error Geocoder::InvalidRequest ||
+                    Geocoder.log(:warn, "#{name} API error: invalid request.")
+      end
+
+      [doc]
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/ipregistry.rb b/lib/geocoder/lookups/ipregistry.rb
new file mode 100644
index 0000000..a0591a4
--- /dev/null
+++ b/lib/geocoder/lookups/ipregistry.rb
@@ -0,0 +1,68 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/ipregistry'
+
+module Geocoder::Lookup
+  class Ipregistry < Base
+
+    ERROR_CODES = {
+      400 => Geocoder::InvalidRequest,
+      401 => Geocoder::InvalidRequest,
+      402 => Geocoder::OverQueryLimitError,
+      403 => Geocoder::InvalidApiKey,
+      451 => Geocoder::RequestDenied,
+      500 => Geocoder::Error
+    }
+    ERROR_CODES.default = Geocoder::Error
+
+    def name
+      "Ipregistry"
+    end
+
+    def supported_protocols
+      [:https, :http]
+    end
+
+    private
+
+    def base_query_url(query)
+      "#{protocol}://#{host}/#{query.sanitized_text}?"
+    end
+
+    def cache_key(query)
+      query_url(query)
+    end
+
+    def host
+      configuration[:host] || "api.ipregistry.co"
+    end
+
+    def query_url_params(query)
+      {
+        key: configuration.api_key
+      }.merge(super)
+    end
+
+    def results(query)
+      # don't look up a loopback or private address, just return the stored result
+      return [reserved_result(query.text)] if query.internal_ip_address?
+
+      return [] unless (doc = fetch_data(query))
+
+      if (error = doc['error'])
+        code = error['code']
+        msg = error['message']
+        raise_error(ERROR_CODES[code], msg ) || Geocoder.log(:warn, "Ipregistry API error: #{msg}")
+        return []
+      end
+      [doc]
+    end
+
+    def reserved_result(ip)
+      {
+        "ip"           => ip,
+        "country_name" => "Reserved",
+        "country_code" => "RD"
+      }
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/latlon.rb b/lib/geocoder/lookups/latlon.rb
index e6a77f2..cdc47ae 100644
--- a/lib/geocoder/lookups/latlon.rb
+++ b/lib/geocoder/lookups/latlon.rb
@@ -25,8 +25,7 @@ module Geocoder::Lookup
       # The API returned a 404 response, which indicates no results found
       elsif doc['error']['type'] == 'api_error'
         []
-      elsif
-        doc['error']['type'] == 'authentication_error'
+      elsif doc['error']['type'] == 'authentication_error'
         raise_error(Geocoder::InvalidApiKey) ||
           Geocoder.log(:warn, "LatLon.io service error: invalid API key.")
       else
diff --git a/lib/geocoder/lookups/location_iq.rb b/lib/geocoder/lookups/location_iq.rb
index 3410557..8ed0387 100644
--- a/lib/geocoder/lookups/location_iq.rb
+++ b/lib/geocoder/lookups/location_iq.rb
@@ -11,6 +11,10 @@ module Geocoder::Lookup
       ["api_key"]
     end
 
+    def supported_protocols
+      [:https]
+    end
+
     private # ----------------------------------------------------------------
 
     def base_query_url(query)
@@ -25,7 +29,7 @@ module Geocoder::Lookup
     end
 
     def configured_host
-      configuration[:host] || "locationiq.org"
+      configuration[:host] || "us1.locationiq.com"
     end
 
     def results(query)
diff --git a/lib/geocoder/lookups/maxmind_local.rb b/lib/geocoder/lookups/maxmind_local.rb
index efce27b..b0d1eac 100644
--- a/lib/geocoder/lookups/maxmind_local.rb
+++ b/lib/geocoder/lookups/maxmind_local.rb
@@ -30,7 +30,13 @@ module Geocoder::Lookup
     def results(query)
       if configuration[:file]
         geoip_class = RUBY_PLATFORM == "java" ? JGeoIP : GeoIP
-        result = geoip_class.new(configuration[:file]).city(query.to_s)
+        geoip_instance = geoip_class.new(configuration[:file])
+        result =
+          if configuration[:package] == :country
+            geoip_instance.country(query.to_s)
+          else
+            geoip_instance.city(query.to_s)
+          end
         result.nil? ? [] : [encode_hash(result.to_hash)]
       elsif configuration[:package] == :city
         addr = IPAddr.new(query.text).to_i
diff --git a/lib/geocoder/lookups/melissa_street.rb b/lib/geocoder/lookups/melissa_street.rb
new file mode 100644
index 0000000..1da9684
--- /dev/null
+++ b/lib/geocoder/lookups/melissa_street.rb
@@ -0,0 +1,41 @@
+require 'geocoder/lookups/base'
+require "geocoder/results/melissa_street"
+
+module Geocoder::Lookup
+  class MelissaStreet < Base
+
+    def name
+      "MelissaStreet"
+    end
+
+    def results(query)
+      return [] unless doc = fetch_data(query)
+
+      if doc["TransmissionResults"] == "GE05"
+        raise_error(Geocoder::InvalidApiKey) ||
+          Geocoder.log(:warn, "Melissa service error: invalid API key.")
+      end
+
+      return doc["Records"]
+    end
+
+    private # ---------------------------------------------------------------
+
+    def base_query_url(query)
+      "#{protocol}://address.melissadata.net/v3/WEB/GlobalAddress/doGlobalAddress?"
+    end
+
+    def query_url_params(query)
+      params = {
+        id: configuration.api_key,
+        format: "JSON",
+        a1: query.sanitized_text,
+        loc: query.options[:city],
+        admarea: query.options[:state],
+        postal: query.options[:postal],
+        ctry: query.options[:country]
+      }
+      params.merge(super)
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/nationaal_georegister_nl.rb b/lib/geocoder/lookups/nationaal_georegister_nl.rb
new file mode 100644
index 0000000..af1256b
--- /dev/null
+++ b/lib/geocoder/lookups/nationaal_georegister_nl.rb
@@ -0,0 +1,38 @@
+require 'geocoder/lookups/base'
+require "geocoder/results/nationaal_georegister_nl"
+
+module Geocoder::Lookup
+  class NationaalGeoregisterNl < Base
+
+    def name
+      'Nationaal Georegister Nederland'
+    end
+
+    private # ---------------------------------------------------------------
+
+    def cache_key(query)
+      base_query_url(query) + hash_to_query(query_url_params(query))
+    end
+
+    def base_query_url(query)
+      "#{protocol}://geodata.nationaalgeoregister.nl/locatieserver/v3/free?"
+    end
+
+    def valid_response?(response)
+      json   = parse_json(response.body)
+      super(response) if json
+    end
+
+    def results(query)
+      return [] unless doc = fetch_data(query)
+      return doc['response']['docs']
+    end
+
+    def query_url_params(query)
+      {
+        fl: '*',
+        q:  query.text
+      }.merge(super)
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/osmnames.rb b/lib/geocoder/lookups/osmnames.rb
new file mode 100644
index 0000000..7a78198
--- /dev/null
+++ b/lib/geocoder/lookups/osmnames.rb
@@ -0,0 +1,57 @@
+require 'cgi'
+require 'geocoder/lookups/base'
+require 'geocoder/results/osmnames'
+
+module Geocoder::Lookup
+  class Osmnames < Base
+
+    def name
+      'OSM Names'
+    end
+
+    def required_api_key_parts
+      configuration[:host] ? [] : ['key']
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    private
+
+    def base_query_url(query)
+      "#{base_url(query)}/#{params_url(query)}.js?"
+    end
+
+    def base_url(query)
+      host = configuration[:host] || 'geocoder.tilehosting.com'
+      "#{protocol}://#{host}"
+    end
+
+    def params_url(query)
+      method, args = 'q', CGI.escape(query.sanitized_text)
+      method, args = 'r', query.coordinates.join('/') if query.reverse_geocode?
+      "#{country_limited(query)}#{method}/#{args}"
+    end
+
+    def results(query)
+      return [] unless doc = fetch_data(query)
+      if (error = doc['message'])
+        raise_error(Geocoder::InvalidRequest, error) ||
+          Geocoder.log(:warn, "OSMNames Geocoding API error: #{error}")
+      else
+        return doc['results']
+      end
+    end
+
+    def query_url_params(query)
+      {
+        key: configuration.api_key
+      }.merge(super)
+    end
+
+    def country_limited(query)
+      "#{query.options[:country_code].downcase}/" if query.options[:country_code] && !query.reverse_geocode?
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/photon.rb b/lib/geocoder/lookups/photon.rb
new file mode 100644
index 0000000..29ad926
--- /dev/null
+++ b/lib/geocoder/lookups/photon.rb
@@ -0,0 +1,89 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/photon'
+
+module Geocoder::Lookup
+  class Photon < Base
+    def name
+      'Photon'
+    end
+
+    private # ---------------------------------------------------------------
+
+    def supported_protocols
+      [:https]
+    end
+
+    def base_query_url(query)
+      host = configuration[:host] || 'photon.komoot.io'
+      method = query.reverse_geocode? ? 'reverse' : 'api'
+      "#{protocol}://#{host}/#{method}?"
+    end
+
+    def results(query)
+      return [] unless (doc = fetch_data(query))
+      return [] unless doc['type'] == 'FeatureCollection'
+      return [] unless doc['features'] || doc['features'].present?
+
+      doc['features']
+    end
+
+    def query_url_params(query)
+      lang = query.language || configuration.language
+      params = { lang: lang, limit: query.options[:limit] }
+
+      if query.reverse_geocode?
+        params.merge!(query_url_params_reverse(query))
+      else
+        params.merge!(query_url_params_coordinates(query))
+      end
+
+      params.merge!(super)
+    end
+
+    def query_url_params_coordinates(query)
+      params = { q: query.sanitized_text }
+
+      if (bias = query.options[:bias])
+        params.merge!(lat: bias[:latitude], lon: bias[:longitude], location_bias_scale: bias[:scale])
+      end
+
+      if (filter = query_url_params_coordinates_filter(query))
+        params.merge!(filter)
+      end
+
+      params
+    end
+
+    def query_url_params_coordinates_filter(query)
+      filter = query.options[:filter]
+      return unless filter
+
+      bbox = filter[:bbox]
+      {
+        bbox: bbox.is_a?(Array) ? bbox.join(',') : bbox,
+        osm_tag: filter[:osm_tag]
+      }
+    end
+
+    def query_url_params_reverse(query)
+      params = { lat: query.coordinates[0], lon: query.coordinates[1], radius: query.options[:radius] }
+
+      if (dsort = query.options[:distance_sort])
+        params[:distance_sort] = dsort ? 'true' : 'false'
+      end
+
+      if (filter = query_url_params_reverse_filter(query))
+        params.merge!(filter)
+      end
+
+      params
+    end
+
+    def query_url_params_reverse_filter(query)
+      filter = query.options[:filter]
+      return unless filter
+
+      { query_string_filter: filter[:string] }
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/pickpoint.rb b/lib/geocoder/lookups/pickpoint.rb
index e298e89..b5863ee 100644
--- a/lib/geocoder/lookups/pickpoint.rb
+++ b/lib/geocoder/lookups/pickpoint.rb
@@ -23,7 +23,7 @@ module Geocoder::Lookup
     end
 
     def query_url_params(query)
-      params = {
+      {
         key: configuration.api_key
       }.merge(super)
     end
diff --git a/lib/geocoder/lookups/smarty_streets.rb b/lib/geocoder/lookups/smarty_streets.rb
index e4f56ec..7745a94 100644
--- a/lib/geocoder/lookups/smarty_streets.rb
+++ b/lib/geocoder/lookups/smarty_streets.rb
@@ -57,7 +57,12 @@ module Geocoder::Lookup
     end
 
     def results(query)
-      fetch_data(query) || []
+      doc = fetch_data(query) || []
+      if doc.is_a?(Hash) and doc.key?('status') # implies there's an error
+        return []
+      else
+        return doc
+      end
     end
   end
 end
diff --git a/lib/geocoder/lookups/telize.rb b/lib/geocoder/lookups/telize.rb
index 2dd8452..839d42e 100644
--- a/lib/geocoder/lookups/telize.rb
+++ b/lib/geocoder/lookups/telize.rb
@@ -16,7 +16,7 @@ module Geocoder::Lookup
       if configuration[:host]
         "#{protocol}://#{configuration[:host]}/location/#{query.sanitized_text}"
       else
-        "#{protocol}://telize-v1.p.mashape.com/location/#{query.sanitized_text}?mashape-key=#{api_key}"
+        "#{protocol}://telize-v1.p.rapidapi.com/location/#{query.sanitized_text}?rapidapi-key=#{api_key}"
       end
     end
 
diff --git a/lib/geocoder/lookups/tencent.rb b/lib/geocoder/lookups/tencent.rb
index 1fef32a..39dcdbf 100644
--- a/lib/geocoder/lookups/tencent.rb
+++ b/lib/geocoder/lookups/tencent.rb
@@ -31,18 +31,18 @@ module Geocoder::Lookup
       case doc['status']
       when 0
         return [doc[content_key]]
-      when 199
-        raise error(Geocoder::InvalidApiKey, "invalid api key") ||
-        Geocoder.log(:warn, "#{name} Geocoding API error: key is not enabled for web service usage.")
-      when 311
-        raise_error(Geocoder::RequestDenied, "request denied") ||
-          Geocoder.log(:warn, "#{name} Geocoding API error: request denied.")
-      when 310, 306
-        raise_error(Geocoder::InvalidRequest, "invalid request.") ||
-          Geocoder.log(:warn, "#{name} Geocoding API error: invalid request.")
       when 311
         raise_error(Geocoder::InvalidApiKey, "invalid api key") ||
           Geocoder.log(:warn, "#{name} Geocoding API error: invalid api key.")
+      when 310
+        raise_error(Geocoder::InvalidRequest, "invalid request.") ||
+          Geocoder.log(:warn, "#{name} Geocoding API error: invalid request, invalid parameters.")
+      when 306
+        raise_error(Geocoder::InvalidRequest, "invalid request.") ||
+          Geocoder.log(:warn, "#{name} Geocoding API error: invalid request, check response for more info.")
+      when 110
+        raise_error(Geocoder::RequestDenied, "request denied.") ||
+          Geocoder.log(:warn, "#{name} Geocoding API error: request source is not authorized.")
       end
       return []
     end
diff --git a/lib/geocoder/lookups/test.rb b/lib/geocoder/lookups/test.rb
index 1a2d0cf..115a626 100644
--- a/lib/geocoder/lookups/test.rb
+++ b/lib/geocoder/lookups/test.rb
@@ -18,6 +18,7 @@ module Geocoder
       end
 
       def self.read_stub(query_text)
+        @default_stub ||= nil
         stubs.fetch(query_text) {
           return @default_stub unless @default_stub.nil?
           raise ArgumentError, "unknown stub request #{query_text}"
@@ -28,6 +29,10 @@ module Geocoder
         @stubs ||= {}
       end
 
+      def self.delete_stub(query_text)
+        stubs.delete(query_text)
+      end
+
       def self.reset
         @stubs = {}
         @default_stub = nil
diff --git a/lib/geocoder/lookups/twogis.rb b/lib/geocoder/lookups/twogis.rb
new file mode 100644
index 0000000..e0dafe5
--- /dev/null
+++ b/lib/geocoder/lookups/twogis.rb
@@ -0,0 +1,58 @@
+require 'geocoder/lookups/base'
+require "geocoder/results/twogis"
+
+module Geocoder::Lookup
+  class Twogis < Base
+
+    def name
+      "2gis"
+    end
+
+    def required_api_key_parts
+      ["key"]
+    end
+
+    def map_link_url(coordinates)
+      "https://2gis.ru/?m=#{coordinates.join(',')}"
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    private # ---------------------------------------------------------------
+
+    def base_query_url(query)
+      "#{protocol}://catalog.api.2gis.com/3.0/items/geocode?"
+    end
+
+    def results(query)
+      return [] unless doc = fetch_data(query)
+      if doc['meta'] && doc['meta']['error']
+        Geocoder.log(:warn, "2gis Geocoding API error: #{doc['meta']["code"]} (#{doc['meta']['error']["message"]}).")
+        return []
+      end
+      if doc['result'] && doc = doc['result']['items']
+        return doc.to_a
+      else
+        Geocoder.log(:warn, "2gis Geocoding API error: unexpected response format.")
+        return []
+      end
+    end
+
+    def query_url_params(query)
+      if query.reverse_geocode?
+        q = query.coordinates.reverse.join(",")
+      else
+        q = query.sanitized_text
+      end
+      params = {
+        :q => q,
+        :lang => "#{query.language || configuration.language}",
+        :key => configuration.api_key,
+        :fields => 'items.street,items.adm_div,items.full_address_name,items.point,items.geometry.centroid'
+      }
+      params.merge(super)
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/uk_ordnance_survey_names.rb b/lib/geocoder/lookups/uk_ordnance_survey_names.rb
new file mode 100644
index 0000000..82e68b6
--- /dev/null
+++ b/lib/geocoder/lookups/uk_ordnance_survey_names.rb
@@ -0,0 +1,59 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/uk_ordnance_survey_names'
+
+module Geocoder::Lookup
+  class UkOrdnanceSurveyNames < Base
+
+    def name
+      'Ordance Survey Names'
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    def base_query_url(query)
+      "#{protocol}://api.os.uk/search/names/v1/find?"
+    end
+
+    def required_api_key_parts
+      ["key"]
+    end
+
+    def query_url(query)
+      base_query_url(query) + url_query_string(query)
+    end
+
+    private # -------------------------------------------------------------
+
+    def results(query)
+      return [] unless doc = fetch_data(query)
+      return [] if doc['header']['totalresults'].zero?
+      return doc['results'].map { |r| r['GAZETTEER_ENTRY'] }
+    end
+
+    def query_url_params(query)
+      {
+        query: query.sanitized_text,
+        key: configuration.api_key,
+        fq: filter
+      }.merge(super)
+    end
+
+    def local_types
+      %w[
+        City
+        Hamlet
+        Other_Settlement
+        Town
+        Village
+        Postcode
+      ]
+    end
+
+    def filter
+      local_types.map { |t| "local_type:#{t}" }.join(' ')
+    end
+
+  end
+end
diff --git a/lib/geocoder/lookups/yandex.rb b/lib/geocoder/lookups/yandex.rb
index 61a6afe..7f91e4d 100644
--- a/lib/geocoder/lookups/yandex.rb
+++ b/lib/geocoder/lookups/yandex.rb
@@ -24,17 +24,16 @@ module Geocoder::Lookup
 
     def results(query)
       return [] unless doc = fetch_data(query)
-      if err = doc['error']
-        if err["status"] == 401 and err["message"] == "invalid key"
+      if [400, 403].include? doc['statusCode']
+        if doc['statusCode'] == 403 and doc['message'] == 'Invalid key'
           raise_error(Geocoder::InvalidApiKey) || Geocoder.log(:warn, "Invalid API key.")
         else
-          Geocoder.log(:warn, "Yandex Geocoding API error: #{err['status']} (#{err['message']}).")
+          Geocoder.log(:warn, "Yandex Geocoding API error: #{doc['statusCode']} (#{doc['message']}).")
         end
         return []
       end
       if doc = doc['response']['GeoObjectCollection']
-        meta = doc['metaDataProperty']['GeocoderResponseMetaData']
-        return meta['found'].to_i > 0 ? doc['featureMember'] : []
+        return doc['featureMember'].to_a
       else
         Geocoder.log(:warn, "Yandex Geocoding API error: unexpected response format.")
         return []
@@ -50,8 +49,8 @@ module Geocoder::Lookup
       params = {
         :geocode => q,
         :format => "json",
-        :plng => "#{query.language || configuration.language}", # supports ru, uk, be
-        :key => configuration.api_key
+        :lang => "#{query.language || configuration.language}", # supports ru, uk, be, default -> ru
+        :apikey => configuration.api_key
       }
       unless (bounds = query.options[:bounds]).nil?
         params[:bbox] = bounds.map{ |point| "%f,%f" % point }.join('~')
diff --git a/lib/geocoder/results/abstract_api.rb b/lib/geocoder/results/abstract_api.rb
new file mode 100644
index 0000000..b8e3068
--- /dev/null
+++ b/lib/geocoder/results/abstract_api.rb
@@ -0,0 +1,146 @@
+require 'geocoder/results/base'
+
+module Geocoder
+  module Result
+    class AbstractApi < Base
+
+      ##
+      # Geolocation
+
+      def state
+        @data['region']
+      end
+
+      def state_code
+        @data['region_iso_code']
+      end
+
+      def city
+        @data["city"]
+      end
+
+      def city_geoname_id
+        @data["city_geoname_id"]
+      end
+
+      def region_geoname_id
+        @data["region_geoname_id"]
+      end
+
+      def postal_code
+        @data["postal_code"]
+      end
+
+      def country
+        @data["country"]
+      end
+
+      def country_code
+        @data["country_code"]
+      end
+
+      def country_geoname_id
+        @data["country_geoname_id"]
+      end
+
+      def country_is_eu
+        @data["country_is_eu"]
+      end
+
+      def continent
+        @data["continent"]
+      end
+
+      def continent_code
+        @data["continent_code"]
+      end
+
+      def continent_geoname_id
+        @data["continent_geoname_id"]
+      end
+
+      ##
+      # Security
+
+      def is_vpn?
+        @data.dig "security", "is_vpn"
+      end
+
+      ##
+      # Timezone
+
+      def timezone_name
+        @data.dig "timezone", "name"
+      end
+
+      def timezone_abbreviation
+        @data.dig "timezone", "abbreviation"
+      end
+
+      def timezone_gmt_offset
+        @data.dig "timezone", "gmt_offset"
+      end
+
+      def timezone_current_time
+        @data.dig "timezone", "current_time"
+      end
+
+      def timezone_is_dst
+        @data.dig "timezone", "is_dst"
+      end
+
+      ##
+      # Flag
+
+      def flag_emoji
+        @data.dig "flag", "emoji"
+      end
+
+      def flag_unicode
+        @data.dig "flag", "unicode"
+      end
+
+      def flag_png
+        @data.dig "flag", "png"
+      end
+
+      def flag_svg
+        @data.dig "flag", "svg"
+      end
+
+      ##
+      # Currency
+
+      def currency_currency_name
+        @data.dig "currency", "currency_name"
+      end
+
+      def currency_currency_code
+        @data.dig "currency", "currency_code"
+      end
+
+      ##
+      # Connection
+
+      def connection_autonomous_system_number
+        @data.dig "connection", "autonomous_system_number"
+      end
+
+      def connection_autonomous_system_organization
+        @data.dig "connection", "autonomous_system_organization"
+      end
+
+      def connection_connection_type
+        @data.dig "connection", "connection_type"
+      end
+
+      def connection_isp_name
+        @data.dig "connection", "isp_name"
+      end
+
+      def connection_organization_name
+        @data.dig "connection", "organization_name"
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/lib/geocoder/results/amazon_location_service.rb b/lib/geocoder/results/amazon_location_service.rb
new file mode 100644
index 0000000..6cc1753
--- /dev/null
+++ b/lib/geocoder/results/amazon_location_service.rb
@@ -0,0 +1,57 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class AmazonLocationService < Base
+    def initialize(result)
+      @place = result
+    end
+
+    def coordinates
+      [@place.geometry.point[1], @place.geometry.point[0]]
+    end
+
+    def address
+      @place.label
+    end
+
+    def neighborhood
+      @place.neighborhood
+    end
+
+    def route
+      @place.street
+    end
+
+    def city
+      @place.municipality || @place.sub_region
+    end
+
+    def state
+      @place.region
+    end
+
+    def state_code
+      @place.region
+    end
+
+    def province
+      @place.region
+    end
+
+    def province_code
+      @place.region
+    end
+
+    def postal_code
+      @place.postal_code
+    end
+
+    def country
+      @place.country
+    end
+
+    def country_code
+      @place.country
+    end
+  end
+end
diff --git a/lib/geocoder/results/baidu.rb b/lib/geocoder/results/baidu.rb
index b30265e..c6fd36e 100644
--- a/lib/geocoder/results/baidu.rb
+++ b/lib/geocoder/results/baidu.rb
@@ -7,10 +7,6 @@ module Geocoder::Result
       ['lat', 'lng'].map{ |i| @data['location'][i] }
     end
 
-    def address
-      @data['formatted_address']
-    end
-
     def province
       @data['addressComponent'] and @data['addressComponent']['province'] or ""
     end
diff --git a/lib/geocoder/results/ban_data_gouv_fr.rb b/lib/geocoder/results/ban_data_gouv_fr.rb
index 0b936b0..d039535 100644
--- a/lib/geocoder/results/ban_data_gouv_fr.rb
+++ b/lib/geocoder/results/ban_data_gouv_fr.rb
@@ -4,10 +4,31 @@ require 'geocoder/results/base'
 module Geocoder::Result
   class BanDataGouvFr < Base
 
+    STATE_CODE_MAPPINGS = {
+      "Guadeloupe" => "01",
+      "Martinique" => "02",
+      "Guyane" => "03",
+      "La Réunion" => "04",
+      "Mayotte" => "06",
+      "Île-de-France" => "11",
+      "Centre-Val de Loire" => "24",
+      "Bourgogne-Franche-Comté" => "27",
+      "Normandie" => "28",
+      "Hauts-de-France" => "32",
+      "Grand Est" => "44",
+      "Pays de la Loire" => "52",
+      "Bretagne" => "53",
+      "Nouvelle-Aquitaine" => "75",
+      "Occitanie" => "76",
+      "Auvergne-Rhône-Alpes" => "84",
+      "Provence-Alpes-Côte d'Azur" => "93",
+      "Corse" => "94"
+    }.freeze
+
     #### BASE METHODS ####
 
     def self.response_attributes
-      %w[limit attribution version licence type features]
+      %w[limit attribution version licence type features center]
     end
 
     response_attributes.each do |a|
@@ -209,6 +230,10 @@ module Geocoder::Result
       end
     end
 
+    def region_code
+      STATE_CODE_MAPPINGS[region_name]
+    end
+
     def country
       "France"
     end
@@ -235,7 +260,7 @@ module Geocoder::Result
     alias_method :street, :street_name
     alias_method :city, :city_name
     alias_method :state, :region_name
-    alias_method :state_code, :state
+    alias_method :state_code, :region_code
 
     #### CITIES' METHODS ####
 
diff --git a/lib/geocoder/results/db_ip_com.rb b/lib/geocoder/results/db_ip_com.rb
index f5291bf..c491d52 100644
--- a/lib/geocoder/results/db_ip_com.rb
+++ b/lib/geocoder/results/db_ip_com.rb
@@ -16,7 +16,7 @@ module Geocoder::Result
     end
 
     def state_code
-      @data['stateProv']
+      @data['stateProvCode']
     end
     alias_method :state, :state_code
 
diff --git a/lib/geocoder/results/esri.rb b/lib/geocoder/results/esri.rb
index cf9c779..b2e3021 100644
--- a/lib/geocoder/results/esri.rb
+++ b/lib/geocoder/results/esri.rb
@@ -16,11 +16,14 @@ module Geocoder::Result
       end
     end
 
-    def state_code
+    def state
       attributes['Region']
     end
 
-    alias_method :state, :state_code
+    def state_code
+      abbr = attributes['RegionAbbr']
+      abbr.to_s == "" ? state : abbr
+    end
 
     def country
       country_key = reverse_geocode? ? "CountryCode" : "Country"
diff --git a/lib/geocoder/results/geoapify.rb b/lib/geocoder/results/geoapify.rb
new file mode 100644
index 0000000..9a8fcdd
--- /dev/null
+++ b/lib/geocoder/results/geoapify.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require 'geocoder/results/base'
+
+module Geocoder
+  module Result
+    # https://apidocs.geoapify.com/docs/geocoding/api
+    class Geoapify < Base
+      def address(_format = :full)
+        properties['formatted']
+      end
+
+      def address_line1
+        properties['address_line1']
+      end
+
+      def address_line2
+        properties['address_line2']
+      end
+
+      def house_number
+        properties['housenumber']
+      end
+
+      def street
+        properties['street']
+      end
+
+      def postal_code
+        properties['postcode']
+      end
+
+      def district
+        properties['district']
+      end
+
+      def suburb
+        properties['suburb']
+      end
+
+      def city
+        properties['city']
+      end
+
+      def county
+        properties['county']
+      end
+
+      def state
+        properties['state']
+      end
+
+      # Not currently available in the API
+      def state_code
+        ''
+      end
+
+      def country
+        properties['country']
+      end
+
+      def country_code
+        return unless properties['country_code']
+
+        properties['country_code'].upcase
+      end
+
+      def coordinates
+        return unless properties['lat']
+        return unless properties['lon']
+
+        [properties['lat'], properties['lon']]
+      end
+
+      # See: https://tools.ietf.org/html/rfc7946#section-3.1
+      #
+      # Each feature has a "Point" type in the Geoapify API.
+      def geometry
+        return unless data['geometry']
+
+        symbol_hash data['geometry']
+      end
+
+      # See: https://tools.ietf.org/html/rfc7946#section-5
+      def bounds
+        data['bbox']
+      end
+
+      # Type of the result, one of:
+      #
+      #   * :unknown
+      #   * :amenity
+      #   * :building
+      #   * :street
+      #   * :suburb
+      #   * :district
+      #   * :postcode
+      #   * :city
+      #   * :county
+      #   * :state
+      #   * :country
+      #
+      def type
+        return :unknown unless properties['result_type']
+
+        properties['result_type'].to_sym
+      end
+
+      # Distance in meters to given bias:proximity or to given coordinates for
+      # reverse geocoding
+      def distance
+        properties['distance']
+      end
+
+      # Calculated rank for the result, containing the following keys:
+      #
+      #   * `popularity` - The popularity score of the result
+      #   * `confidence` - The confidence value of the result (0-1)
+      #   * `match_type` - The result's match type, one of following:
+      #      * full_match
+      #      * inner_part
+      #      * match_by_building
+      #      * match_by_street
+      #      * match_by_postcode
+      #      * match_by_city_or_disrict
+      #      * match_by_country_or_state
+      #
+      # Example:
+      #   {
+      #     popularity: 8.615793062435909,
+      #     confidence: 0.88,
+      #     match_type: :full_match
+      #   }
+      def rank
+        return unless properties['rank']
+
+        r = symbol_hash(properties['rank'])
+        r[:match_type] = r[:match_type].to_sym if r[:match_type]
+        r
+      end
+
+      # Examples:
+      #
+      # Open
+      #   {
+      #     sourcename: 'openstreetmap',
+      #     wheelchair: 'limited',
+      #     wikidata: 'Q186125',
+      #     wikipedia: 'en:Madison Square Garden',
+      #     website: 'http://www.thegarden.com/',
+      #     phone: '12124656741',
+      #     osm_type: 'W',
+      #     osm_id: 138141251,
+      #     continent: 'North America',
+      #   }
+      def datasource
+        return unless properties['datasource']
+
+        symbol_hash properties['datasource']
+      end
+
+      private
+
+      def properties
+        @properties ||= data['properties'] || {}
+      end
+
+      def symbol_hash(orig_hash)
+        {}.tap do |result|
+          orig_hash.each_key do |key|
+            next unless orig_hash[key]
+
+            result[key.to_sym] = orig_hash[key]
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/geocoder_us.rb b/lib/geocoder/results/geocoder_us.rb
deleted file mode 100644
index ca20ad4..0000000
--- a/lib/geocoder/results/geocoder_us.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'geocoder/results/base'
-
-module Geocoder::Result
-  class GeocoderUs < Base
-    def coordinates
-      [@data[0].to_f, @data[1].to_f]
-    end
-
-    def address(format = :full)
-      "#{street_address}, #{city}, #{state} #{postal_code}, #{country}".sub(/^[ ,]*/, "")
-    end
-
-    def street_address
-      @data[2]
-    end
-
-    def city
-      @data[3]
-    end
-
-    def state
-      @data[4]
-    end
-
-    alias_method :state_code, :state
-
-    def postal_code
-      @data[5]
-    end
-
-    def country
-      'United States'
-    end
-
-    def country_code
-      'US'
-    end
-  end
-end
diff --git a/lib/geocoder/results/here.rb b/lib/geocoder/results/here.rb
index 12f3ab8..2b32f39 100644
--- a/lib/geocoder/results/here.rb
+++ b/lib/geocoder/results/here.rb
@@ -7,73 +7,71 @@ module Geocoder::Result
     # A string in the given format.
     #
     def address(format = :full)
-      address_data['Label']
+      address_data["label"]
     end
 
     ##
     # A two-element array: [lat, lon].
     #
     def coordinates
-      fail unless d = @data['Location']['DisplayPosition']
-      [d['Latitude'].to_f, d['Longitude'].to_f]
+      fail unless d = @data["position"]
+      [d["lat"].to_f, d["lng"].to_f]
     end
     
     def route
-      address_data['Street']
+      address_data["street"]
     end
     
     def street_number
-      address_data['HouseNumber']
+      address_data["houseNumber"]
     end  
 
     def state
-      address_data['County']
+      address_data["state"]
     end
 
     def province
-      address_data['County']
+      address_data["county"]
     end
 
     def postal_code
-      address_data['PostalCode']
+      address_data["postalCode"]
     end
 
     def city
-      address_data['City']
+      address_data["city"]
     end
 
     def state_code
-      address_data['State']
+      address_data["stateCode"]
     end
 
     def province_code
-      address_data['State']
+      address_data["state"]
     end
 
     def country
-      fail unless d = address_data['AdditionalData']
-      if v = d.find{|ad| ad['key']=='CountryName'}
-        return v['value']
-      end
+      address_data["countryName"]
     end
 
     def country_code
-      address_data['Country']
+      address_data["countryCode"]
     end
 
     def viewport
-      map_view = data['Location']['MapView'] || fail
-      south = map_view['BottomRight']['Latitude']
-      west = map_view['TopLeft']['Longitude']
-      north = map_view['TopLeft']['Latitude']
-      east = map_view['BottomRight']['Longitude']
+      return [] if data["resultType"] == "place"
+      map_view = data["mapView"]
+      south = map_view["south"]
+      west = map_view["west"]
+      north = map_view["north"]
+      east = map_view["east"]
       [south, west, north, east]
     end
 
     private # ----------------------------------------------------------------
 
     def address_data
-      @data['Location']['Address'] || fail
+      @data["address"] || fail
     end
   end
 end
diff --git a/lib/geocoder/results/ipbase.rb b/lib/geocoder/results/ipbase.rb
new file mode 100644
index 0000000..00dc3b5
--- /dev/null
+++ b/lib/geocoder/results/ipbase.rb
@@ -0,0 +1,40 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Ipbase < Base
+    def ip
+      @data["data"]['ip']
+    end
+
+    def country_code
+      @data["data"]["location"]["country"]["alpha2"]
+    end
+
+    def country
+      @data["data"]["location"]["country"]["name"]
+    end
+
+    def state_code
+      @data["data"]["location"]["region"]["alpha2"]
+    end
+
+    def state
+      @data["data"]["location"]["region"]["name"]
+    end
+
+    def city
+      @data["data"]["location"]["city"]["name"]
+    end
+
+    def postal_code
+      @data["data"]["location"]["zip"]
+    end
+
+    def coordinates
+      [
+        @data["data"]["location"]["latitude"].to_f,
+        @data["data"]["location"]["longitude"].to_f
+      ]
+    end
+  end
+end
diff --git a/lib/geocoder/results/ipgeolocation.rb b/lib/geocoder/results/ipgeolocation.rb
new file mode 100644
index 0000000..15dd05b
--- /dev/null
+++ b/lib/geocoder/results/ipgeolocation.rb
@@ -0,0 +1,59 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Ipgeolocation < Base
+
+    def coordinates
+      [@data['latitude'].to_f, @data['longitude'].to_f]
+    end
+
+    def address(format = :full)
+      "#{city}, #{state} #{postal_code}, #{country_name}".sub(/^[ ,]*/, "")
+    end
+
+    def state
+      @data['state_prov']
+    end
+
+    def state_code
+      @data['state_prov']
+    end
+
+    def country
+      @data['country_name']
+    end
+
+    def country_code
+      @data['country_code2']
+    end
+
+    def postal_code
+      @data['zipcode']
+    end
+
+    def self.response_attributes
+      [
+          ['ip', ''],
+          ['hostname', ''],
+          ['continent_code', ''],
+          ['continent_name', ''],
+          ['country_code2', ''],
+          ['country_code3', ''],
+          ['country_name', ''],
+          ['country_capital',''],
+          ['district',''],
+          ['state_prov',''],
+          ['city', ''],
+          ['zipcode', ''],
+          ['time_zone', {}],
+          ['currency', {}]
+      ]
+    end
+
+    response_attributes.each do |attr, default|
+      define_method attr do
+        @data[attr] || default
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/ipqualityscore.rb b/lib/geocoder/results/ipqualityscore.rb
new file mode 100644
index 0000000..c2bf58d
--- /dev/null
+++ b/lib/geocoder/results/ipqualityscore.rb
@@ -0,0 +1,54 @@
+require 'geocoder/results/base'
+
+module Geocoder
+  module Result
+    class Ipqualityscore < Base
+
+      def self.key_method_mappings
+        {
+          'request_id' => :request_id,
+          'success' => :success?,
+          'message' => :message,
+          'city' => :city,
+          'region' => :state,
+          'country_code' => :country_code,
+          'mobile' => :mobile?,
+          'fraud_score' => :fraud_score,
+          'ISP' => :isp,
+          'ASN' => :asn,
+          'organization' => :organization,
+          'is_crawler' => :crawler?,
+          'host' => :host,
+          'proxy' => :proxy?,
+          'vpn' => :vpn?,
+          'tor' => :tor?,
+          'active_vpn' => :active_vpn?,
+          'active_tor' => :active_tor?,
+          'recent_abuse' => :recent_abuse?,
+          'bot_status' => :bot?,
+          'connection_type' => :connection_type,
+          'abuse_velocity' => :abuse_velocity,
+          'timezone' => :timezone,
+        }
+      end
+
+      key_method_mappings.each_pair do |key, meth|
+        define_method meth do
+          @data[key]
+        end
+      end
+
+      alias_method :state_code, :state
+      alias_method :country, :country_code
+
+      def postal_code
+        '' # No suitable fallback
+      end
+
+      def address
+        [city, state, country_code].compact.reject(&:empty?).join(', ')
+      end
+
+    end
+  end
+end
diff --git a/lib/geocoder/results/ipregistry.rb b/lib/geocoder/results/ipregistry.rb
new file mode 100644
index 0000000..7179559
--- /dev/null
+++ b/lib/geocoder/results/ipregistry.rb
@@ -0,0 +1,304 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Ipregistry < Base
+
+    def initialize(data)
+      super
+
+      @data = flatten_hash(data)
+    end
+
+    def coordinates
+      [@data['location_latitude'], @data['location_longitude']]
+    end
+
+    def flatten_hash(hash)
+      hash.each_with_object({}) do |(k, v), h|
+        if v.is_a? Hash
+          flatten_hash(v).map do |h_k, h_v|
+            h["#{k}_#{h_k}".to_s] = h_v
+          end
+        else
+          h[k] = v
+        end
+      end
+    end
+
+    private :flatten_hash
+
+    def city
+      @data['location_city']
+    end
+
+    def country
+      @data['location_country_name']
+    end
+
+    def country_code
+      @data['location_country_code']
+    end
+
+    def postal_code
+      @data['location_postal']
+    end
+
+    def state
+      @data['location_region_name']
+    end
+
+    def state_code
+      @data['location_region_code']
+    end
+
+    # methods for fields specific to Ipregistry
+
+    def ip
+      @data["ip"]
+    end
+
+    def type
+      @data["type"]
+    end
+
+    def hostname
+      @data["hostname"]
+    end
+
+    def carrier_name
+      @data["carrier_name"]
+    end
+
+    def carrier_mcc
+      @data["carrier_mcc"]
+    end
+
+    def carrier_mnc
+      @data["carrier_mnc"]
+    end
+
+    def connection_asn
+      @data["connection_asn"]
+    end
+
+    def connection_domain
+      @data["connection_domain"]
+    end
+
+    def connection_organization
+      @data["connection_organization"]
+    end
+
+    def connection_type
+      @data["connection_type"]
+    end
+
+    def currency_code
+      @data["currency_code"]
+    end
+
+    def currency_name
+      @data["currency_name"]
+    end
+
+    def currency_plural
+      @data["currency_plural"]
+    end
+
+    def currency_symbol
+      @data["currency_symbol"]
+    end
+
+    def currency_symbol_native
+      @data["currency_symbol_native"]
+    end
+
+    def currency_format_negative_prefix
+      @data["currency_format_negative_prefix"]
+    end
+
+    def currency_format_negative_suffix
+      @data["currency_format_negative_suffix"]
+    end
+
+    def currency_format_positive_prefix
+      @data["currency_format_positive_prefix"]
+    end
+
+    def currency_format_positive_suffix
+      @data["currency_format_positive_suffix"]
+    end
+
+    def location_continent_code
+      @data["location_continent_code"]
+    end
+
+    def location_continent_name
+      @data["location_continent_name"]
+    end
+
+    def location_country_area
+      @data["location_country_area"]
+    end
+
+    def location_country_borders
+      @data["location_country_borders"]
+    end
+
+    def location_country_calling_code
+      @data["location_country_calling_code"]
+    end
+
+    def location_country_capital
+      @data["location_country_capital"]
+    end
+
+    def location_country_code
+      @data["location_country_code"]
+    end
+
+    def location_country_name
+      @data["location_country_name"]
+    end
+
+    def location_country_population
+      @data["location_country_population"]
+    end
+
+    def location_country_population_density
+      @data["location_country_population_density"]
+    end
+
+    def location_country_flag_emoji
+      @data["location_country_flag_emoji"]
+    end
+
+    def location_country_flag_emoji_unicode
+      @data["location_country_flag_emoji_unicode"]
+    end
+
+    def location_country_flag_emojitwo
+      @data["location_country_flag_emojitwo"]
+    end
+
+    def location_country_flag_noto
+      @data["location_country_flag_noto"]
+    end
+
+    def location_country_flag_twemoji
+      @data["location_country_flag_twemoji"]
+    end
+
+    def location_country_flag_wikimedia
+      @data["location_country_flag_wikimedia"]
+    end
+
+    def location_country_languages
+      @data["location_country_languages"]
+    end
+
+    def location_country_tld
+      @data["location_country_tld"]
+    end
+
+    def location_region_code
+      @data["location_region_code"]
+    end
+
+    def location_region_name
+      @data["location_region_name"]
+    end
+
+    def location_city
+      @data["location_city"]
+    end
+
+    def location_postal
+      @data["location_postal"]
+    end
+
+    def location_latitude
+      @data["location_latitude"]
+    end
+
+    def location_longitude
+      @data["location_longitude"]
+    end
+
+    def location_language_code
+      @data["location_language_code"]
+    end
+
+    def location_language_name
+      @data["location_language_name"]
+    end
+
+    def location_language_native
+      @data["location_language_native"]
+    end
+
+    def location_in_eu
+      @data["location_in_eu"]
+    end
+
+    def security_is_bogon
+      @data["security_is_bogon"]
+    end
+
+    def security_is_cloud_provider
+      @data["security_is_cloud_provider"]
+    end
+
+    def security_is_tor
+      @data["security_is_tor"]
+    end
+
+    def security_is_tor_exit
+      @data["security_is_tor_exit"]
+    end
+
+    def security_is_proxy
+      @data["security_is_proxy"]
+    end
+
+    def security_is_anonymous
+      @data["security_is_anonymous"]
+    end
+
+    def security_is_abuser
+      @data["security_is_abuser"]
+    end
+
+    def security_is_attacker
+      @data["security_is_attacker"]
+    end
+
+    def security_is_threat
+      @data["security_is_threat"]
+    end
+
+    def time_zone_id
+      @data["time_zone_id"]
+    end
+
+    def time_zone_abbreviation
+      @data["time_zone_abbreviation"]
+    end
+
+    def time_zone_current_time
+      @data["time_zone_current_time"]
+    end
+
+    def time_zone_name
+      @data["time_zone_name"]
+    end
+
+    def time_zone_offset
+      @data["time_zone_offset"]
+    end
+
+    def time_zone_in_daylight_saving
+      @data["time_zone_in_daylight_saving"]
+    end
+  end
+end
diff --git a/lib/geocoder/results/mapbox.rb b/lib/geocoder/results/mapbox.rb
index e458731..a43e5e2 100644
--- a/lib/geocoder/results/mapbox.rb
+++ b/lib/geocoder/results/mapbox.rb
@@ -23,7 +23,10 @@ module Geocoder::Result
       context_part('region')
     end
 
-    alias_method :state_code, :state
+    def state_code
+      value = context_part('region', 'short_code')
+      value.split('-').last unless value.nil?
+    end
 
     def postal_code
       context_part('postcode')
@@ -33,7 +36,10 @@ module Geocoder::Result
       context_part('country')
     end
 
-    alias_method :country_code, :country
+    def country_code
+      value = context_part('country', 'short_code')
+      value.upcase unless value.nil?
+    end
 
     def neighborhood
       context_part('neighborhood')
@@ -45,8 +51,8 @@ module Geocoder::Result
 
     private
 
-    def context_part(name)
-      context.map { |c| c['text'] if c['id'] =~ Regexp.new(name) }.compact.first
+    def context_part(name, key = 'text')
+      (context.detect { |c| c['id'] =~ Regexp.new(name) } || {})[key]
     end
 
     def context
diff --git a/lib/geocoder/results/melissa_street.rb b/lib/geocoder/results/melissa_street.rb
new file mode 100644
index 0000000..8cf6079
--- /dev/null
+++ b/lib/geocoder/results/melissa_street.rb
@@ -0,0 +1,46 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class MelissaStreet < Base
+    def address(format = :full)
+      @data['FormattedAddress']
+    end
+
+    def street_address
+      @data['AddressLine1']
+    end
+
+    def suffix
+      @data['ThoroughfareTrailingType']
+    end
+
+    def number
+      @data['PremisesNumber']
+    end
+
+    def city
+      @data['Locality']
+    end
+
+    def state_code
+      @data['AdministrativeArea']
+    end
+    alias_method :state, :state_code
+
+    def country
+      @data['CountryName']
+    end
+
+    def country_code
+      @data['CountryISO3166_1_Alpha2']
+    end
+
+    def postal_code
+      @data['PostalCode']
+    end
+
+    def coordinates
+      [@data['Latitude'].to_f, @data['Longitude'].to_f]
+    end
+  end
+end
diff --git a/lib/geocoder/results/nationaal_georegister_nl.rb b/lib/geocoder/results/nationaal_georegister_nl.rb
new file mode 100644
index 0000000..429e506
--- /dev/null
+++ b/lib/geocoder/results/nationaal_georegister_nl.rb
@@ -0,0 +1,62 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class NationaalGeoregisterNl < Base
+
+    def response_attributes
+      @data
+    end
+
+    def coordinates
+      @data['centroide_ll'][6..-2].split(' ').map(&:to_f).reverse
+    end
+
+    def formatted_address
+      @data['weergavenaam']
+    end
+
+    alias_method :address, :formatted_address
+
+    def province
+      @data['provincienaam']
+    end
+
+    alias_method :state, :province
+
+    def city
+      @data['woonplaatsnaam']
+    end
+
+    def district
+      @data['gemeentenaam']
+    end
+
+    def street
+      @data['straatnaam']
+    end
+
+    def street_number
+      @data['huis_nlt']
+    end
+
+    def address_components
+      @data
+    end
+
+    def state_code
+      @data['provinciecode']
+    end
+
+    def postal_code
+      @data['postcode']
+    end
+
+    def country
+      "Netherlands"
+    end
+
+    def country_code
+      "NL"
+    end
+  end
+end
diff --git a/lib/geocoder/results/nominatim.rb b/lib/geocoder/results/nominatim.rb
index c993425..9f6149a 100644
--- a/lib/geocoder/results/nominatim.rb
+++ b/lib/geocoder/results/nominatim.rb
@@ -4,12 +4,12 @@ module Geocoder::Result
   class Nominatim < Base
 
     def poi
-      return @data['address'][place_type] if @data['address'].key?(place_type)
+      return address_data[place_type] if address_data.key?(place_type)
       return nil
     end
 
     def house_number
-      @data['address']['house_number']
+      address_data['house_number']
     end
 
     def address
@@ -18,65 +18,71 @@ module Geocoder::Result
 
     def street
       %w[road pedestrian highway].each do |key|
-        return @data['address'][key] if @data['address'].key?(key)
+        return address_data[key] if address_data.key?(key)
       end
       return nil
     end
 
     def city
       %w[city town village hamlet].each do |key|
-        return @data['address'][key] if @data['address'].key?(key)
+        return address_data[key] if address_data.key?(key)
       end
       return nil
     end
 
     def village
-      @data['address']['village']
+      address_data['village']
     end
 
     def town
-      @data['address']['town']
+      address_data['town']
     end
 
     def state
-      @data['address']['state']
+      address_data['state']
     end
 
     alias_method :state_code, :state
 
     def postal_code
-      @data['address']['postcode']
+      address_data['postcode']
     end
 
     def county
-      @data['address']['county']
+      address_data['county']
     end
 
     def country
-      @data['address']['country']
+      address_data['country']
     end
 
     def country_code
-      @data['address']['country_code']
+      address_data['country_code']
     end
 
     def suburb
-      @data['address']['suburb']
+      address_data['suburb']
     end
 
     def city_district
-      @data['address']['city_district']
+      address_data['city_district']
     end
 
     def state_district
-      @data['address']['state_district']
+      address_data['state_district']
     end
 
     def neighbourhood
-      @data['address']['neighbourhood']
+      address_data['neighbourhood']
+    end
+
+    def municipality
+      address_data['municipality']
     end
 
     def coordinates
+      return [] unless @data['lat'] && @data['lon']
+
       [@data['lat'].to_f, @data['lon'].to_f]
     end
 
@@ -105,5 +111,11 @@ module Geocoder::Result
         end
       end
     end
+
+    private
+
+    def address_data
+      @data['address'] || {}
+    end
   end
 end
diff --git a/lib/geocoder/results/osmnames.rb b/lib/geocoder/results/osmnames.rb
new file mode 100644
index 0000000..5954e75
--- /dev/null
+++ b/lib/geocoder/results/osmnames.rb
@@ -0,0 +1,56 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Osmnames < Base
+    def address
+      @data['display_name']
+    end
+
+    def coordinates
+      [@data['lat'].to_f, @data['lon'].to_f]
+    end
+
+    def viewport
+      west, south, east, north = @data['boundingbox'].map(&:to_f)
+      [south, west, north, east]
+    end
+
+    def state
+      @data['state']
+    end
+    alias_method :state_code, :state
+
+    def place_class
+      @data['class']
+    end
+
+    def place_type
+      @data['type']
+    end
+
+    def postal_code
+      ''
+    end
+
+    def country_code
+      @data['country_code']
+    end
+
+    def country
+      @data['country']
+    end
+
+    def self.response_attributes
+      %w[house_number street city name osm_id osm_type boundingbox place_rank
+      importance county rank name_suffix]
+    end
+
+    response_attributes.each do |a|
+      unless method_defined?(a)
+        define_method a do
+          @data[a]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/photon.rb b/lib/geocoder/results/photon.rb
new file mode 100644
index 0000000..534f0d0
--- /dev/null
+++ b/lib/geocoder/results/photon.rb
@@ -0,0 +1,119 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Photon < Base
+    def name
+      properties['name']
+    end
+
+    def address(_format = :full)
+      parts = []
+      parts << name if name
+      parts << street_address if street_address
+      parts << city
+      parts << state if state
+      parts << postal_code
+      parts << country
+
+      parts.join(', ')
+    end
+
+    def street_address
+      return unless street
+      return street unless house_number
+
+      "#{house_number} #{street}"
+    end
+
+    def house_number
+      properties['housenumber']
+    end
+
+    def street
+      properties['street']
+    end
+
+    def postal_code
+      properties['postcode']
+    end
+
+    def city
+      properties['city']
+    end
+
+    def state
+      properties['state']
+    end
+
+    def state_code
+      ''
+    end
+
+    def country
+      properties['country']
+    end
+
+    def country_code
+      ''
+    end
+
+    def coordinates
+      return unless geometry
+      return unless geometry[:coordinates]
+
+      geometry[:coordinates].reverse
+    end
+
+    def geometry
+      return unless data['geometry']
+
+      symbol_hash data['geometry']
+    end
+
+    def bounds
+      properties['extent']
+    end
+
+    # Type of the result (OSM object type), one of:
+    #
+    #   :node
+    #   :way
+    #   :relation
+    #
+    def type
+      {
+        'N' => :node,
+        'W' => :way,
+        'R' => :relation
+      }[properties['osm_type']]
+    end
+
+    def osm_id
+      properties['osm_id']
+    end
+
+    # See: https://wiki.openstreetmap.org/wiki/Tags
+    def osm_tag
+      return unless properties['osm_key']
+      return properties['osm_key'] unless properties['osm_value']
+
+      "#{properties['osm_key']}=#{properties['osm_value']}"
+    end
+
+    private
+
+    def properties
+      @properties ||= data['properties'] || {}
+    end
+
+    def symbol_hash(orig_hash)
+      {}.tap do |result|
+        orig_hash.each_key do |key|
+          next unless orig_hash[key]
+
+          result[key.to_sym] = orig_hash[key]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/twogis.rb b/lib/geocoder/results/twogis.rb
new file mode 100644
index 0000000..766be2d
--- /dev/null
+++ b/lib/geocoder/results/twogis.rb
@@ -0,0 +1,76 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Twogis < Base
+    def coordinates
+      ['lat', 'lon'].map{ |i| @data['point'][i] } if @data['point']
+    end
+
+    def address(_format = :full)
+      @data['full_address_name'] || ''
+    end
+
+    def city
+      return '' unless @data['adm_div']
+      @data['adm_div'].select{|u| u["type"] == "city"}.first.try(:[], 'name') || ''
+    end
+
+    def region
+      return '' unless @data['adm_div']
+      @data['adm_div'].select{|u| u["type"] == "region"}.first.try(:[], 'name') || ''
+    end
+
+    def country
+      return '' unless @data['adm_div']
+      @data['adm_div'].select{|u| u["type"] == "country"}.first.try(:[], 'name') || ''
+    end
+
+    def district
+      return '' unless @data['adm_div']
+      @data['adm_div'].select{|u| u["type"] == "district"}.first.try(:[], 'name') || ''
+    end
+
+    def district_area
+      return '' unless @data['adm_div']
+      @data['adm_div'].select{|u| u["type"] == "district_area"}.first.try(:[], 'name') || ''
+    end
+
+    def street_address
+      @data['address_name'] || ''
+    end
+
+    def street
+      return '' unless @data['address_name']
+      @data['address_name'].split(', ').first
+    end
+
+    def street_number
+      return '' unless @data['address_name']
+      @data['address_name'].split(', ')[1] || ''
+    end
+
+    def type
+      @data['type'] || ''
+    end
+
+    def purpose_name
+      @data['purpose_name'] || ''
+    end
+
+    def building_name
+      @data['building_name'] || ''
+    end
+
+    def subtype
+      @data['subtype'] || ''
+    end
+
+    def subtype_specification
+      @data['subtype_specification'] || ''
+    end
+
+    def name
+      @data['name'] || ''
+    end
+  end
+end
diff --git a/lib/geocoder/results/uk_ordnance_survey_names.rb b/lib/geocoder/results/uk_ordnance_survey_names.rb
new file mode 100644
index 0000000..c09d357
--- /dev/null
+++ b/lib/geocoder/results/uk_ordnance_survey_names.rb
@@ -0,0 +1,59 @@
+require 'geocoder/results/base'
+require 'easting_northing'
+
+module Geocoder::Result
+  class UkOrdnanceSurveyNames < Base
+
+    def coordinates
+      @coordinates ||= Geocoder::EastingNorthing.new(
+        easting: data['GEOMETRY_X'],
+        northing: data['GEOMETRY_Y'],
+      ).lat_lng
+    end
+
+    def city
+      is_postcode? ? data['DISTRICT_BOROUGH'] : data['NAME1']
+    end
+
+    def county
+      data['COUNTY_UNITARY']
+    end
+    alias state county
+
+    def county_code
+      code_from_uri data['COUNTY_UNITARY_URI']
+    end
+    alias state_code county_code
+
+    def province
+      data['REGION']
+    end
+
+    def province_code
+      code_from_uri data['REGION_URI']
+    end
+
+    def postal_code
+      is_postcode? ? data['NAME1'] : ''
+    end
+
+    def country
+      'United Kingdom'
+    end
+
+    def country_code
+      'UK'
+    end
+
+    private
+
+    def is_postcode?
+      data['LOCAL_TYPE'] == 'Postcode'
+    end
+
+    def code_from_uri(uri)
+      return '' if uri.nil?
+      uri.split('/').last
+    end
+  end
+end
diff --git a/lib/geocoder/results/yandex.rb b/lib/geocoder/results/yandex.rb
index 5243533..eced227 100644
--- a/lib/geocoder/results/yandex.rb
+++ b/lib/geocoder/results/yandex.rb
@@ -2,78 +2,223 @@ require 'geocoder/results/base'
 
 module Geocoder::Result
   class Yandex < Base
+    # Yandex result has difficult tree structure,
+    # and presence of some nodes depends on exact search case.
+
+    # Also Yandex lacks documentation about it.
+    # See https://tech.yandex.com/maps/doc/geocoder/desc/concepts/response_structure-docpage/
+
+    # Ultimatly, we need to find Locality and/or Thoroughfare data.
+
+    # It may resides on the top (ADDRESS_DETAILS) level.
+    # example: 'Baltic Sea'
+    # "AddressDetails": {
+    #   "Locality": {
+    #     "Premise": {
+    #       "PremiseName": "Baltic Sea"
+    #     }
+    #   }
+    # }
+
+    ADDRESS_DETAILS = %w[
+      GeoObject metaDataProperty GeocoderMetaData
+      AddressDetails
+    ].freeze
+
+    # On COUNTRY_LEVEL.
+    # example: 'Potomak'
+    # "AddressDetails": {
+    #   "Country": {
+    #     "AddressLine": "reka Potomak",
+    #     "CountryNameCode": "US",
+    #     "CountryName": "United States of America",
+    #     "Locality": {
+    #       "Premise": {
+    #         "PremiseName": "reka Potomak"
+    #       }
+    #     }
+    #   }
+    # }
+
+    COUNTRY_LEVEL = %w[
+      GeoObject metaDataProperty GeocoderMetaData
+      AddressDetails Country
+    ].freeze
+
+    # On ADMIN_LEVEL (usually state or city)
+    # example: 'Moscow, Tverskaya'
+    # "AddressDetails": {
+    #   "Country": {
+    #     "AddressLine": "Moscow, Tverskaya Street",
+    #     "CountryNameCode": "RU",
+    #     "CountryName": "Russia",
+    #     "AdministrativeArea": {
+    #       "AdministrativeAreaName": "Moscow",
+    #       "Locality": {
+    #         "LocalityName": "Moscow",
+    #         "Thoroughfare": {
+    #           "ThoroughfareName": "Tverskaya Street"
+    #         }
+    #       }
+    #     }
+    #   }
+    # }
+
+    ADMIN_LEVEL = %w[
+      GeoObject metaDataProperty GeocoderMetaData
+      AddressDetails Country
+      AdministrativeArea
+    ].freeze
+
+    # On SUBADMIN_LEVEL (may refer to urban district)
+    # example: 'Moscow Region, Krasnogorsk'
+    # "AddressDetails": {
+    #   "Country": {
+    #     "AddressLine": "Moscow Region, Krasnogorsk",
+    #     "CountryNameCode": "RU",
+    #     "CountryName": "Russia",
+    #     "AdministrativeArea": {
+    #       "AdministrativeAreaName": "Moscow Region",
+    #       "SubAdministrativeArea": {
+    #         "SubAdministrativeAreaName": "gorodskoy okrug Krasnogorsk",
+    #         "Locality": {
+    #           "LocalityName": "Krasnogorsk"
+    #         }
+    #       }
+    #     }
+    #   }
+    # }
+
+    SUBADMIN_LEVEL = %w[
+      GeoObject metaDataProperty GeocoderMetaData
+      AddressDetails Country
+      AdministrativeArea
+      SubAdministrativeArea
+    ].freeze
+
+    # On DEPENDENT_LOCALITY_1 (may refer to district of city)
+    # example: 'Paris, Etienne Marcel'
+    # "AddressDetails": {
+    #   "Country": {
+    #     "AddressLine": "Île-de-France, Paris, 1er Arrondissement, Rue Étienne Marcel",
+    #     "CountryNameCode": "FR",
+    #     "CountryName": "France",
+    #     "AdministrativeArea": {
+    #       "AdministrativeAreaName": "Île-de-France",
+    #       "Locality": {
+    #         "LocalityName": "Paris",
+    #         "DependentLocality": {
+    #           "DependentLocalityName": "1er Arrondissement",
+    #           "Thoroughfare": {
+    #             "ThoroughfareName": "Rue Étienne Marcel"
+    #           }
+    #         }
+    #       }
+    #     }
+    #   }
+    # }
+
+    DEPENDENT_LOCALITY_1 = %w[
+      GeoObject metaDataProperty GeocoderMetaData
+      AddressDetails Country
+      AdministrativeArea Locality
+      DependentLocality
+    ].freeze
+
+    # On DEPENDENT_LOCALITY_2 (for special cases like turkish "mahalle")
+    # https://en.wikipedia.org/wiki/Mahalle
+    # example: 'Istanbul Mabeyinci Yokuşu 17'
+
+    # "AddressDetails": {
+    #   "Country": {
+    #     "AddressLine": "İstanbul, Fatih, Saraç İshak Mah., Mabeyinci Yokuşu, 17",
+    #     "CountryNameCode": "TR",
+    #     "CountryName": "Turkey",
+    #     "AdministrativeArea": {
+    #       "AdministrativeAreaName": "İstanbul",
+    #       "SubAdministrativeArea": {
+    #         "SubAdministrativeAreaName": "Fatih",
+    #         "Locality": {
+    #           "DependentLocality": {
+    #             "DependentLocalityName": "Saraç İshak Mah.",
+    #             "Thoroughfare": {
+    #               "ThoroughfareName": "Mabeyinci Yokuşu",
+    #               "Premise": {
+    #                 "PremiseNumber": "17"
+    #               }
+    #             }
+    #           }
+    #         }
+    #       }
+    #     }
+    #   }
+    # }
+
+    DEPENDENT_LOCALITY_2 = %w[
+      GeoObject metaDataProperty GeocoderMetaData
+      AddressDetails Country
+      AdministrativeArea
+      SubAdministrativeArea Locality
+      DependentLocality
+    ].freeze
 
     def coordinates
       @data['GeoObject']['Point']['pos'].split(' ').reverse.map(&:to_f)
     end
 
-    def address(format = :full)
+    def address(_format = :full)
       @data['GeoObject']['metaDataProperty']['GeocoderMetaData']['text']
     end
 
     def city
-      if state.empty? and address_details and address_details.has_key? 'Locality'
-        address_details['Locality']['LocalityName']
-      elsif sub_state.empty? and address_details and address_details.has_key? 'AdministrativeArea' and
-          address_details['AdministrativeArea'].has_key? 'Locality'
-        address_details['AdministrativeArea']['Locality']['LocalityName']
-      elsif not sub_state_city.empty?
-        sub_state_city
-      else
-        ""
-      end
+      result =
+        if state.empty?
+          find_in_hash(@data, *COUNTRY_LEVEL, 'Locality', 'LocalityName')
+        elsif sub_state.empty?
+          find_in_hash(@data, *ADMIN_LEVEL, 'Locality', 'LocalityName')
+        else
+          find_in_hash(@data, *SUBADMIN_LEVEL, 'Locality', 'LocalityName')
+        end
+
+      result || ""
     end
 
     def country
-      if address_details
-        address_details['CountryName']
-      else
-        ""
-      end
+      find_in_hash(@data, *COUNTRY_LEVEL, 'CountryName') || ""
     end
 
     def country_code
-      if address_details
-        address_details['CountryNameCode']
-      else
-        ""
-      end
+      find_in_hash(@data, *COUNTRY_LEVEL, 'CountryNameCode') || ""
     end
 
     def state
-      if address_details and address_details['AdministrativeArea']
-        address_details['AdministrativeArea']['AdministrativeAreaName']
-      else
-        ""
-      end
+      find_in_hash(@data, *ADMIN_LEVEL, 'AdministrativeAreaName') || ""
     end
 
     def sub_state
-      if !state.empty? and address_details and address_details['AdministrativeArea']['SubAdministrativeArea']
-        address_details['AdministrativeArea']['SubAdministrativeArea']['SubAdministrativeAreaName']
-      else
-        ""
-      end
+      return "" if state.empty?
+      find_in_hash(@data, *SUBADMIN_LEVEL, 'SubAdministrativeAreaName') || ""
     end
 
     def state_code
       ""
     end
 
-    def postal_code
-      ""
+    def street
+      thoroughfare_data.is_a?(Hash) ? thoroughfare_data['ThoroughfareName'] : ""
     end
 
-    def premise_name
-      address_details['Locality']['Premise']['PremiseName']
+    def street_number
+      premise.is_a?(Hash) ? premise.fetch('PremiseNumber', "") : ""
     end
 
-    def street
-      thoroughfare_data && thoroughfare_data['ThoroughfareName']
+    def premise_name
+      premise.is_a?(Hash) ? premise.fetch('PremiseName', "") : ""
     end
 
-    def street_number
-      thoroughfare_data && thoroughfare_data['Premise'] && thoroughfare_data['Premise']['PremiseNumber']
+    def postal_code
+      return "" unless premise.is_a?(Hash)
+      find_in_hash(premise, 'PostalCode', 'PostalCodeNumber') || ""
     end
 
     def kind
@@ -93,42 +238,55 @@ module Geocoder::Result
 
     private # ----------------------------------------------------------------
 
-    def thoroughfare_data
-      locality_data && locality_data['Thoroughfare']
+    def top_level_locality
+      find_in_hash(@data, *ADDRESS_DETAILS, 'Locality')
     end
 
-    def locality_data
-      dependent_locality && subadmin_locality && admin_locality
+    def country_level_locality
+      find_in_hash(@data, *COUNTRY_LEVEL, 'Locality')
     end
 
     def admin_locality
-      address_details && address_details['AdministrativeArea'] &&
-        address_details['AdministrativeArea']['Locality']
+      find_in_hash(@data, *ADMIN_LEVEL, 'Locality')
     end
 
     def subadmin_locality
-      address_details && address_details['AdministrativeArea'] &&
-        address_details['AdministrativeArea']['SubAdministrativeArea'] &&
-        address_details['AdministrativeArea']['SubAdministrativeArea']['Locality']
+      find_in_hash(@data, *SUBADMIN_LEVEL, 'Locality')
     end
 
     def dependent_locality
-      address_details && address_details['AdministrativeArea'] &&
-        address_details['AdministrativeArea']['SubAdministrativeArea'] &&
-        address_details['AdministrativeArea']['SubAdministrativeArea']['Locality'] &&
-        address_details['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']
+      find_in_hash(@data, *DEPENDENT_LOCALITY_1) ||
+        find_in_hash(@data, *DEPENDENT_LOCALITY_2)
+    end
+
+    def locality_data
+      dependent_locality || subadmin_locality || admin_locality ||
+        country_level_locality || top_level_locality
+    end
+
+    def thoroughfare_data
+      locality_data['Thoroughfare'] if locality_data.is_a?(Hash)
     end
 
-    def address_details
-      @data['GeoObject']['metaDataProperty']['GeocoderMetaData']['AddressDetails']['Country']
+    def premise
+      if thoroughfare_data.is_a?(Hash)
+        thoroughfare_data['Premise']
+      elsif locality_data.is_a?(Hash)
+        locality_data['Premise']
+      end
     end
 
-    def sub_state_city
-      if !sub_state.empty? and address_details and address_details['AdministrativeArea']['SubAdministrativeArea'].has_key? 'Locality'
-        address_details['AdministrativeArea']['SubAdministrativeArea']['Locality']['LocalityName'] || ""
-      else
-        ""
+    def find_in_hash(source, *keys)
+      key = keys.shift
+      result = source[key]
+
+      if keys.empty?
+        return result
+      elsif !result.is_a?(Hash)
+        return nil
       end
+
+      find_in_hash(result, *keys)
     end
   end
 end
diff --git a/lib/geocoder/sql.rb b/lib/geocoder/sql.rb
index 71ea96f..6bca8a6 100644
--- a/lib/geocoder/sql.rb
+++ b/lib/geocoder/sql.rb
@@ -44,13 +44,13 @@ module Geocoder
     end
 
     def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr)
-      spans = "#{lat_attr} BETWEEN #{sw_lat} AND #{ne_lat} AND "
+      spans = "#{lat_attr} BETWEEN #{sw_lat.to_f} AND #{ne_lat.to_f} AND "
       # handle box that spans 180 longitude
       if sw_lng.to_f > ne_lng.to_f
-        spans + "(#{lon_attr} BETWEEN #{sw_lng} AND 180 OR " +
-        "#{lon_attr} BETWEEN -180 AND #{ne_lng})"
+        spans + "(#{lon_attr} BETWEEN #{sw_lng.to_f} AND 180 OR " +
+        "#{lon_attr} BETWEEN -180 AND #{ne_lng.to_f})"
       else
-        spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
+        spans + "#{lon_attr} BETWEEN #{sw_lng.to_f} AND #{ne_lng.to_f}"
       end
     end
 
diff --git a/lib/geocoder/util.rb b/lib/geocoder/util.rb
new file mode 100644
index 0000000..30ca5cd
--- /dev/null
+++ b/lib/geocoder/util.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Geocoder
+  module Util
+    #
+    # Recursive version of Hash#merge!
+    #
+    # Adds the contents of +h2+ to +h1+,
+    # merging entries in +h1+ with duplicate keys with those from +h2+.
+    #
+    # Compared with Hash#merge!, this method supports nested hashes.
+    # When both +h1+ and +h2+ contains an entry with the same key,
+    # it merges and returns the values from both hashes.
+    #
+    #    h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
+    #    h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
+    #    recursive_hash_merge(h1, h2)   #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
+    #
+    # Simply using Hash#merge! would return
+    #
+    #    h1.merge!(h2)    #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
+    #
+    def self.recursive_hash_merge(h1, h2)
+      h1.merge!(h2) do |_key, oldval, newval|
+        oldval.class == h1.class ? self.recursive_hash_merge(oldval, newval) : newval
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/version.rb b/lib/geocoder/version.rb
index 14ea8f9..418aab9 100644
--- a/lib/geocoder/version.rb
+++ b/lib/geocoder/version.rb
@@ -1,3 +1,3 @@
 module Geocoder
-  VERSION = "1.5.1"
+  VERSION = "1.8.1"
 end
diff --git a/lib/hash_recursive_merge.rb b/lib/hash_recursive_merge.rb
deleted file mode 100644
index 163566e..0000000
--- a/lib/hash_recursive_merge.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# 
-# = Hash Recursive Merge
-# 
-# Merges a Ruby Hash recursively, Also known as deep merge.
-# Recursive version of Hash#merge and Hash#merge!.
-# 
-# Category::    Ruby
-# Package::     Hash
-# Author::      Simone Carletti <weppos@weppos.net>
-# Copyright::   2007-2008 The Authors
-# License::     MIT License
-# Link::        http://www.simonecarletti.com/
-# Source::      http://gist.github.com/gists/6391/
-#
-module HashRecursiveMerge
-
-  #
-  # Recursive version of Hash#merge!
-  # 
-  # Adds the contents of +other_hash+ to +hsh+, 
-  # merging entries in +hsh+ with duplicate keys with those from +other_hash+.
-  # 
-  # Compared with Hash#merge!, this method supports nested hashes.
-  # When both +hsh+ and +other_hash+ contains an entry with the same key,
-  # it merges and returns the values from both arrays.
-  # 
-  #    h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
-  #    h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
-  #    h1.rmerge!(h2)   #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
-  #    
-  # Simply using Hash#merge! would return
-  # 
-  #    h1.merge!(h2)    #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
-  # 
-  def rmerge!(other_hash)
-    merge!(other_hash) do |key, oldval, newval|
-      oldval.class == self.class ? oldval.rmerge!(newval) : newval
-    end
-  end
-
-  #
-  # Recursive version of Hash#merge
-  # 
-  # Compared with Hash#merge!, this method supports nested hashes.
-  # When both +hsh+ and +other_hash+ contains an entry with the same key,
-  # it merges and returns the values from both arrays.
-  # 
-  # Compared with Hash#merge, this method provides a different approch
-  # for merging nasted hashes.
-  # If the value of a given key is an Hash and both +other_hash+ abd +hsh
-  # includes the same key, the value is merged instead replaced with
-  # +other_hash+ value.
-  # 
-  #    h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
-  #    h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
-  #    h1.rmerge(h2)    #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
-  #    
-  # Simply using Hash#merge would return
-  # 
-  #    h1.merge(h2)     #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
-  # 
-  def rmerge(other_hash)
-    r = {}
-    merge(other_hash) do |key, oldval, newval|
-      r[key] = oldval.class == self.class ? oldval.rmerge(newval) : newval
-    end
-  end
-
-end
-
-
-class Hash
-  include HashRecursiveMerge
-end
diff --git a/lib/maxmind_database.rb b/lib/maxmind_database.rb
index 471195a..9330f03 100644
--- a/lib/maxmind_database.rb
+++ b/lib/maxmind_database.rb
@@ -96,9 +96,9 @@ module Geocoder
 
     def archive_url_path(package)
       {
-        geolite_country_csv: "GeoIPCountryCSV.zip",
-        geolite_city_csv: "GeoLiteCity_CSV/GeoLiteCity-latest.zip",
-        geolite_asn_csv: "asnum/GeoIPASNum2.zip"
+        geolite_country_csv: "GeoLite2-Country-CSV.zip",
+        geolite_city_csv: "GeoLite2-City-CSV.zip",
+        geolite_asn_csv: "GeoLite2-ASN-CSV.zip"
       }[package]
     end
 
diff --git a/test/database.yml b/test/database.yml
new file mode 100644
index 0000000..f5f96de
--- /dev/null
+++ b/test/database.yml
@@ -0,0 +1,14 @@
+# test/database.yml
+sqlite:
+  adapter: sqlite3
+  database: ":memory:"
+  timeout: 500
+mysql:
+  adapter: mysql2
+  database: geocoder_test
+  username: travis
+  encoding: utf8
+postgres:
+  adapter: postgresql
+  database: geocoder_test
+  username: postgres
diff --git a/test/db/migrate/001_create_test_schema.rb b/test/db/migrate/001_create_test_schema.rb
new file mode 100644
index 0000000..dfbf7b8
--- /dev/null
+++ b/test/db/migrate/001_create_test_schema.rb
@@ -0,0 +1,60 @@
+# CreateTestSchema creates the tables used in test_helper.rb
+
+superclass = ActiveRecord::Migration
+# TODO: Inherit from the 5.0 Migration class directly when we drop support for Rails 4.
+superclass = ActiveRecord::Migration[5.0] if superclass.respond_to?(:[])
+
+class CreateTestSchema < superclass
+  def self.up
+    [
+      :places,
+      :place_reverse_geocodeds
+    ].each do |table|
+      create_table table do |t|
+        t.column :name, :string
+        t.column :address, :string
+        t.column :latitude, :decimal, :precision => 16, :scale => 6
+        t.column :longitude, :decimal, :precision => 16, :scale => 6
+        t.column :radius_column, :decimal, :precision => 16, :scale => 6
+      end
+    end
+
+    [
+      :place_with_custom_lookup_procs,
+      :place_with_custom_lookups,
+      :place_reverse_geocoded_with_custom_lookups
+    ].each do |table|
+      create_table table do |t|
+        t.column :name, :string
+        t.column :address, :string
+        t.column :latitude, :decimal, :precision => 16, :scale => 6
+        t.column :longitude, :decimal, :precision => 16, :scale => 6
+        t.column :result_class, :string
+      end
+    end
+
+    create_table :place_with_forward_and_reverse_geocodings do |t|
+      t.column :name, :string
+      t.column :location, :string
+      t.column :lat, :decimal, :precision => 16, :scale => 6
+      t.column :lon, :decimal, :precision => 16, :scale => 6
+      t.column :address, :string
+    end
+
+    create_table :place_reverse_geocoded_with_custom_results_handlings do |t|
+      t.column :name, :string
+      t.column :address, :string
+      t.column :latitude, :decimal, :precision => 16, :scale => 6
+      t.column :longitude, :decimal, :precision => 16, :scale => 6
+      t.column :country, :string
+    end
+
+    create_table :place_with_custom_results_handlings do |t|
+      t.column :name, :string
+      t.column :address, :string
+      t.column :latitude, :decimal, :precision => 16, :scale => 6
+      t.column :longitude, :decimal, :precision => 16, :scale => 6
+      t.column :coords_string, :string
+    end
+  end
+end
diff --git a/test/fixtures/abstract_api b/test/fixtures/abstract_api
new file mode 100644
index 0000000..51f7fff
--- /dev/null
+++ b/test/fixtures/abstract_api
@@ -0,0 +1,45 @@
+{
+  "ip_address": "2.19.128.50",
+  "city": "Seattle",
+  "city_geoname_id": 5809844,
+  "region": "Washington",
+  "region_iso_code": "WA",
+  "region_geoname_id": 5815135,
+  "postal_code": "98111",
+  "country": "United States",
+  "country_code": "US",
+  "country_geoname_id": 6252001,
+  "country_is_eu": false,
+  "continent": "North America",
+  "continent_code": "NA",
+  "continent_geoname_id": 6255149,
+  "longitude": -122.3412,
+  "latitude": 47.6032,
+  "security": {
+    "is_vpn": false
+  },
+  "timezone": {
+    "name": "America/Los_Angeles",
+    "abbreviation": "PST",
+    "gmt_offset": -8,
+    "current_time": "06:06:14",
+    "is_dst": false
+  },
+  "flag": {
+    "emoji": "🇺🇸",
+    "unicode": "U+1F1FA U+1F1F8",
+    "png": "https://static.abstractapi.com/country-flags/US_flag.png",
+    "svg": "https://static.abstractapi.com/country-flags/US_flag.svg"
+  },
+  "currency": {
+    "currency_name": "USD",
+    "currency_code": "USD"
+  },
+  "connection": {
+    "autonomous_system_number": 16625,
+    "autonomous_system_organization": "AKAMAI-AS",
+    "connection_type": "Corporate",
+    "isp_name": "Akamai Technologies",
+    "organization_name": null
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/abstract_api_2_19_128_50 b/test/fixtures/abstract_api_2_19_128_50
new file mode 100644
index 0000000..51f7fff
--- /dev/null
+++ b/test/fixtures/abstract_api_2_19_128_50
@@ -0,0 +1,45 @@
+{
+  "ip_address": "2.19.128.50",
+  "city": "Seattle",
+  "city_geoname_id": 5809844,
+  "region": "Washington",
+  "region_iso_code": "WA",
+  "region_geoname_id": 5815135,
+  "postal_code": "98111",
+  "country": "United States",
+  "country_code": "US",
+  "country_geoname_id": 6252001,
+  "country_is_eu": false,
+  "continent": "North America",
+  "continent_code": "NA",
+  "continent_geoname_id": 6255149,
+  "longitude": -122.3412,
+  "latitude": 47.6032,
+  "security": {
+    "is_vpn": false
+  },
+  "timezone": {
+    "name": "America/Los_Angeles",
+    "abbreviation": "PST",
+    "gmt_offset": -8,
+    "current_time": "06:06:14",
+    "is_dst": false
+  },
+  "flag": {
+    "emoji": "🇺🇸",
+    "unicode": "U+1F1FA U+1F1F8",
+    "png": "https://static.abstractapi.com/country-flags/US_flag.png",
+    "svg": "https://static.abstractapi.com/country-flags/US_flag.svg"
+  },
+  "currency": {
+    "currency_name": "USD",
+    "currency_code": "USD"
+  },
+  "connection": {
+    "autonomous_system_number": 16625,
+    "autonomous_system_organization": "AKAMAI-AS",
+    "connection_type": "Corporate",
+    "isp_name": "Akamai Technologies",
+    "organization_name": null
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/amap_invalid_key b/test/fixtures/amap_invalid_key
new file mode 100644
index 0000000..590b88b
--- /dev/null
+++ b/test/fixtures/amap_invalid_key
@@ -0,0 +1 @@
+{"status":"0","info":"INVALID_USER_KEY","infocode":"10001"}
\ No newline at end of file
diff --git a/test/fixtures/amap_no_results b/test/fixtures/amap_no_results
new file mode 100644
index 0000000..295f888
--- /dev/null
+++ b/test/fixtures/amap_no_results
@@ -0,0 +1,7 @@
+{
+  "status": "1",
+  "info": "OK",
+  "infocode": "10000",
+  "count": "0",
+  "geocodes": []
+}
\ No newline at end of file
diff --git a/test/fixtures/amap_reverse b/test/fixtures/amap_reverse
new file mode 100644
index 0000000..6728ee8
--- /dev/null
+++ b/test/fixtures/amap_reverse
@@ -0,0 +1,38 @@
+{
+  "status": "1",
+  "info": "OK",
+  "infocode": "10000",
+  "regeocode": {
+    "formatted_address": "Canada Ontario University Private ",
+    "addressComponent": {
+      "country": "Canada",
+      "province": "Ontario",
+      "city": [],
+      "citycode": [],
+      "district": [],
+      "adcode": [],
+      "township": "University Private",
+      "towncode": [],
+      "neighborhood": {
+        "name": [],
+        "type": []
+      },
+      "building": {
+        "name": [],
+        "type": []
+      },
+      "streetNumber": {
+        "street": "University Private",
+        "number": [],
+        "location": "-75.680763,45.426723",
+        "direction": [],
+        "distance": []
+      },
+      "businessAreas": []
+    },
+    "pois": [],
+    "roads": [],
+    "roadinters": [],
+    "aois": []
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/amap_shanghai_pearl_tower b/test/fixtures/amap_shanghai_pearl_tower
new file mode 100644
index 0000000..7d153af
--- /dev/null
+++ b/test/fixtures/amap_shanghai_pearl_tower
@@ -0,0 +1,29 @@
+{
+  "status": "1",
+  "info": "OK",
+  "infocode": "10000",
+  "count": "1",
+  "geocodes": [
+    {
+      "formatted_address": "上海市浦东新区明珠电视塔",
+      "province": "上海市",
+      "citycode": "021",
+      "city": "上海市",
+      "district": "浦东新区",
+      "township": [],
+      "neighborhood": {
+        "name": [],
+        "type": []
+      },
+      "building": {
+        "name": [],
+        "type": []
+      },
+      "adcode": "310115",
+      "street": [],
+      "number": [],
+      "location": "121.499567,31.239950",
+      "level": "兴趣点"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/fixtures/amazon_location_service_madison_square_garden b/test/fixtures/amazon_location_service_madison_square_garden
new file mode 100644
index 0000000..48dbce4
--- /dev/null
+++ b/test/fixtures/amazon_location_service_madison_square_garden
@@ -0,0 +1,12 @@
+[
+  nil,
+  "USA",
+  MockAWSPlaceGeometry.new([-74.15434739412053, 40.61681535865544]),
+  "Madison Ave, Staten Island, NY, 10314, USA",
+  "Staten Island",
+  "Graniteville",
+  "10314",
+  "New York",
+  "Madison Ave",
+  "Richmond County",
+]
diff --git a/test/fixtures/baidu_invalid_key b/test/fixtures/baidu_invalid_key
new file mode 100644
index 0000000..9e70656
--- /dev/null
+++ b/test/fixtures/baidu_invalid_key
@@ -0,0 +1 @@
+{"results":[],"status":5,"msg":"AK Illegal or Not Exist:"}
diff --git a/test/fixtures/baidu_ip_202_198_16_3 b/test/fixtures/baidu_ip_202_198_16_3
new file mode 100644
index 0000000..10a4d5f
--- /dev/null
+++ b/test/fixtures/baidu_ip_202_198_16_3
@@ -0,0 +1,19 @@
+{
+  "address": "CN|北京|北京|None|CHINANET|1|None",
+  "content": {
+    "address": "北京市", 
+    "address_detail": {
+      "city": "北京市",
+      "city_code": 131,
+    "district": "", 
+    "province": "北京市",
+    "street": "",
+    "street_number": ""
+    },
+    "point": {
+      "x": "116.39564504",
+      "y": "39.92998578"
+    }
+  },
+  "status": 0
+} 
diff --git a/test/fixtures/baidu_ip_invalid_key b/test/fixtures/baidu_ip_invalid_key
new file mode 100644
index 0000000..211f926
--- /dev/null
+++ b/test/fixtures/baidu_ip_invalid_key
@@ -0,0 +1 @@
+{"status":5,"uid":null,"sk":null,"logformat":null}
diff --git a/test/fixtures/baidu_ip_no_results b/test/fixtures/baidu_ip_no_results
new file mode 100644
index 0000000..95ae1ed
--- /dev/null
+++ b/test/fixtures/baidu_ip_no_results
@@ -0,0 +1 @@
+{"status":0, "content":{}}
diff --git a/test/fixtures/baidu_no_results b/test/fixtures/baidu_no_results
new file mode 100644
index 0000000..1ab1ab3
--- /dev/null
+++ b/test/fixtures/baidu_no_results
@@ -0,0 +1 @@
+{"status":0,"result":[]}
diff --git a/test/fixtures/baidu_reverse b/test/fixtures/baidu_reverse
new file mode 100644
index 0000000..bf6dcf0
--- /dev/null
+++ b/test/fixtures/baidu_reverse
@@ -0,0 +1 @@
+{"status":0,"result":{"location":{"lng":121.48789948569,"lat":31.249161555654},"formatted_address":"上海市闸北区天潼路619号","business":"七浦路,海宁路,北京东路","addressComponent":{"city":"上海市","district":"闸北区","province":"上海市","street":"天潼路","street_number":"619号"},"cityCode":289}}
diff --git a/test/fixtures/baidu_shanghai_pearl_tower b/test/fixtures/baidu_shanghai_pearl_tower
new file mode 100644
index 0000000..f5619fe
--- /dev/null
+++ b/test/fixtures/baidu_shanghai_pearl_tower
@@ -0,0 +1,12 @@
+{
+	"status":0,
+		"result":{
+			"location":{
+				"lng":116.30814954222,
+				"lat":40.056885091681
+			},
+			"precise":1,
+			"confidence":80,
+			"level":"\u5546\u52a1\u5927\u53a6"
+		}
+}
diff --git a/test/fixtures/ban_data_gouv_fr_montpellier b/test/fixtures/ban_data_gouv_fr_montpellier
new file mode 100644
index 0000000..a026add
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_montpellier
@@ -0,0 +1,125 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "Montpellier",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34080",
+        "context": "34, Hérault, Occitanie",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172_34080",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34000",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34090",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172_34090",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34070",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172_34070",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          -0.744328,
+          45.634502
+        ]
+      },
+      "properties": {
+        "citycode": "17244",
+        "adm_weight": "1",
+        "name": "Montpellier-de-Médillan",
+        "city": "Montpellier-de-Médillan",
+        "postcode": "17260",
+        "context": "17, Charente-Maritime, Poitou-Charentes",
+        "score": 0.825,
+        "label": "Montpellier-de-Médillan",
+        "id": "17244",
+        "type": "village",
+        "population": "0.6"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/ban_data_gouv_fr_no_results b/test/fixtures/ban_data_gouv_fr_no_results
new file mode 100644
index 0000000..3c2e783
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_no_results
@@ -0,0 +1,9 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "oozpuip",
+  "type": "FeatureCollection",
+  "features": []
+}
diff --git a/test/fixtures/ban_data_gouv_fr_no_reverse_results b/test/fixtures/ban_data_gouv_fr_no_reverse_results
new file mode 100644
index 0000000..657d238
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_no_reverse_results
@@ -0,0 +1,8 @@
+{
+  "limit": 1,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "type": "FeatureCollection",
+  "features": []
+}
diff --git a/test/fixtures/ban_data_gouv_fr_paris b/test/fixtures/ban_data_gouv_fr_paris
new file mode 100644
index 0000000..a1e46a0
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_paris
@@ -0,0 +1,117 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "Paris",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.3469,
+          48.8589
+        ]
+      },
+      "properties": {
+        "adm_weight": "6",
+        "citycode": "75056",
+        "name": "Paris",
+        "city": "Paris",
+        "postcode": "75000",
+        "context": "75, Île-de-France",
+        "score": 1,
+        "label": "Paris",
+        "id": "75056",
+        "type": "city",
+        "population": "2244"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.564293,
+          45.766413
+        ]
+      },
+      "properties": {
+        "citycode": "63125",
+        "postcode": "63120",
+        "name": "Paris",
+        "city": "Courpière",
+        "context": "63, Puy-de-Dôme, Auvergne",
+        "score": 0.8255363636363636,
+        "label": "Paris 63120 Courpière",
+        "id": "63125_B221_03549b",
+        "type": "locality"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          1.550208,
+          44.673592
+        ]
+      },
+      "properties": {
+        "citycode": "46138",
+        "postcode": "46240",
+        "name": "PARIS (Vaillac)",
+        "city": "Cœur de Causse",
+        "context": "46, Lot, Midi-Pyrénées",
+        "score": 0.824090909090909,
+        "label": "PARIS (Vaillac) 46240 Cœur de Causse",
+        "id": "46138_XXXX_6ee4ec",
+        "type": "street"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          -0.526884,
+          43.762253
+        ]
+      },
+      "properties": {
+        "citycode": "40282",
+        "postcode": "40500",
+        "name": "Paris",
+        "city": "Saint-Sever",
+        "context": "40, Landes, Aquitaine",
+        "score": 0.8236181818181818,
+        "label": "Paris 40500 Saint-Sever",
+        "id": "40282_B237_2364e3",
+        "type": "locality"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          0.157613,
+          47.336685
+        ]
+      },
+      "properties": {
+        "citycode": "37031",
+        "postcode": "37140",
+        "name": "Paris Buton",
+        "city": "Bourgueil",
+        "context": "37, Indre-et-Loire, Centre Val-de-Loire",
+        "score": 0.8235454545454545,
+        "label": "Paris Buton 37140 Bourgueil",
+        "id": "37031_B165_0a5e7a",
+        "type": "locality"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/ban_data_gouv_fr_reverse b/test/fixtures/ban_data_gouv_fr_reverse
new file mode 100644
index 0000000..b3a1b68
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_reverse
@@ -0,0 +1,33 @@
+{
+  "limit": 1,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.364375,
+          48.770639
+        ]
+      },
+      "properties": {
+        "street": "Rue du Lieutenant Alain le Coz",
+        "label": "4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue",
+        "distance": 23,
+        "context": "94, Val-de-Marne, Île-de-France",
+        "id": "94021_1133_49638b",
+        "citycode": "94021",
+        "name": "4 Rue du Lieutenant Alain le Coz",
+        "city": "Chevilly-Larue",
+        "postcode": "94550",
+        "housenumber": "4",
+        "score": 0.9999997696809948,
+        "type": "housenumber"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/ban_data_gouv_fr_rue_yves_toudic b/test/fixtures/ban_data_gouv_fr_rue_yves_toudic
new file mode 100644
index 0000000..dea2d42
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_rue_yves_toudic
@@ -0,0 +1,33 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "13 rue yves toudic 75010 Paris",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.363473,
+          48.870131
+        ]
+      },
+      "properties": {
+        "citycode": "75110",
+        "postcode": "75010",
+        "name": "13 Rue Yves Toudic",
+        "housenumber": "13",
+        "city": "Paris",
+        "context": "75, Île-de-France",
+        "score": 0.9437454545454544,
+        "label": "13 Rue Yves Toudic 75010 Paris",
+        "id": "ADRNIVX_0000000270748760",
+        "type": "housenumber",
+        "street": "Rue Yves Toudic"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/bing_forbidden_request b/test/fixtures/bing_forbidden_request
new file mode 100644
index 0000000..6736e6a
--- /dev/null
+++ b/test/fixtures/bing_forbidden_request
@@ -0,0 +1,16 @@
+{
+   "authenticationResultCode":"ValidCredentials",
+   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",
+   "copyright":"Copyright © 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+   "resourceSets":[
+      {
+         "estimatedTotal":0,
+         "resources":[
+
+         ]
+      }
+   ],
+   "statusCode":403,
+   "statusDescription":"OK",
+   "traceId":"907b76a307bc49129a489de3d4c992ea|CH1M001463|02.00.82.2800|CH1MSNVM001383, CH1MSNVM001358, CH1MSNVM001397"
+}
diff --git a/test/fixtures/bing_internal_server_error b/test/fixtures/bing_internal_server_error
new file mode 100644
index 0000000..8abd2c4
--- /dev/null
+++ b/test/fixtures/bing_internal_server_error
@@ -0,0 +1,16 @@
+{
+   "authenticationResultCode":"ValidCredentials",
+   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",
+   "copyright":"Copyright © 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+   "resourceSets":[
+      {
+         "estimatedTotal":0,
+         "resources":[
+
+         ]
+      }
+   ],
+   "statusCode":500,
+   "statusDescription":"OK",
+   "traceId":"907b76a307bc49129a489de3d4c992ea|CH1M001463|02.00.82.2800|CH1MSNVM001383, CH1MSNVM001358, CH1MSNVM001397"
+}
diff --git a/test/fixtures/bing_invalid_key b/test/fixtures/bing_invalid_key
new file mode 100644
index 0000000..d34e1e9
--- /dev/null
+++ b/test/fixtures/bing_invalid_key
@@ -0,0 +1 @@
+{"authenticationResultCode":"InvalidCredentials","brandLogoUri":"http:\\/\\/dev.virtualearth.net\\/Branding\\/logo_powered_by.png","copyright":"Copyright \xC2\xA9 2012 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.","errorDetails":["Access was denied. You may have entered your credentials incorrectly, or you might not have access to the requested resource or operation."],"resourceSets":[],"statusCode":401,"statusDescription":"Unauthorized","traceId":"5c539f6e70c44b2e858741b6c932318e|EWRM001670|02.00.83.1900|"}
diff --git a/test/fixtures/bing_madison_square_garden b/test/fixtures/bing_madison_square_garden
new file mode 100644
index 0000000..d24e06e
--- /dev/null
+++ b/test/fixtures/bing_madison_square_garden
@@ -0,0 +1,40 @@
+{
+   "authenticationResultCode":"ValidCredentials",
+   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",
+   "copyright":"Copyright © 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+   "resourceSets":[
+      {
+         "estimatedTotal":1,
+         "resources":[
+            {
+               "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1",
+               "bbox":[
+                  40.744944289326668,
+                  -74.002353921532631,
+                  40.755675807595253,
+                  -73.983625397086143
+               ],
+               "name":"Madison Square Garden, NY",
+               "point":{
+                  "type":"Point",
+                  "coordinates":[
+                     40.75031,
+                     -73.99299
+                  ]
+               },
+               "address":{
+                  "adminDistrict":"NY",
+                  "countryRegion":"United States",
+                  "formattedAddress":"Madison Square Garden, NY",
+                  "locality":"New York"
+               },
+               "confidence":"High",
+               "entityType":"Stadium"
+            }
+         ]
+      }
+   ],
+   "statusCode":200,
+   "statusDescription":"OK",
+   "traceId":"55094ee53c8d45e789794014666328cd|CH1M001466|02.00.82.2800|CH1MSNVM001396, CH1MSNVM001370, CH1MSNVM001397"
+}
diff --git a/test/fixtures/bing_no_results b/test/fixtures/bing_no_results
new file mode 100644
index 0000000..43135d6
--- /dev/null
+++ b/test/fixtures/bing_no_results
@@ -0,0 +1,16 @@
+{
+   "authenticationResultCode":"ValidCredentials",
+   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",
+   "copyright":"Copyright © 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+   "resourceSets":[
+      {
+         "estimatedTotal":0,
+         "resources":[
+
+         ]
+      }
+   ],
+   "statusCode":200,
+   "statusDescription":"OK",
+   "traceId":"907b76a307bc49129a489de3d4c992ea|CH1M001463|02.00.82.2800|CH1MSNVM001383, CH1MSNVM001358, CH1MSNVM001397"
+}
diff --git a/test/fixtures/bing_reverse b/test/fixtures/bing_reverse
new file mode 100644
index 0000000..443213c
--- /dev/null
+++ b/test/fixtures/bing_reverse
@@ -0,0 +1,42 @@
+{
+   "authenticationResultCode":"ValidCredentials",
+   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",
+   "copyright":"Copyright © 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+   "resourceSets":[
+      {
+         "estimatedTotal":1,
+         "resources":[
+            {
+               "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1",
+               "bbox":[
+                  45.419835111675845,
+                  -75.683656128790716,
+                  45.4275605468172,
+                  -75.66898098334994
+               ],
+               "name":"291 Rue Somerset E, Ottawa, ON, K1N",
+               "point":{
+                  "type":"Point",
+                  "coordinates":[
+                     45.423697829246521,
+                     -75.676318556070328
+                  ]
+               },
+               "address":{
+                  "addressLine":"291 Rue Somerset E",
+                  "adminDistrict":"ON",
+                  "countryRegion":"Canada",
+                  "formattedAddress":"291 Rue Somerset E, Ottawa, ON, K1N",
+                  "locality":"Ottawa",
+                  "postalCode":"K1N"
+               },
+               "confidence":"Medium",
+               "entityType":"Address"
+            }
+         ]
+      }
+   ],
+   "statusCode":200,
+   "statusDescription":"OK",
+   "traceId":"27bd5ed659e64ba6970c4144f1d4ea94|CH1M001470|02.00.82.2800|CH1MSNVM001396, CH1MSNVM001374"
+}
diff --git a/test/fixtures/bing_service_unavailable b/test/fixtures/bing_service_unavailable
new file mode 100644
index 0000000..43135d6
--- /dev/null
+++ b/test/fixtures/bing_service_unavailable
@@ -0,0 +1,16 @@
+{
+   "authenticationResultCode":"ValidCredentials",
+   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",
+   "copyright":"Copyright © 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+   "resourceSets":[
+      {
+         "estimatedTotal":0,
+         "resources":[
+
+         ]
+      }
+   ],
+   "statusCode":200,
+   "statusDescription":"OK",
+   "traceId":"907b76a307bc49129a489de3d4c992ea|CH1M001463|02.00.82.2800|CH1MSNVM001383, CH1MSNVM001358, CH1MSNVM001397"
+}
diff --git a/test/fixtures/cloudmade_invalid_key b/test/fixtures/cloudmade_invalid_key
new file mode 100644
index 0000000..0b133da
--- /dev/null
+++ b/test/fixtures/cloudmade_invalid_key
@@ -0,0 +1 @@
+Forbidden request
diff --git a/test/fixtures/cloudmade_madison_square_garden b/test/fixtures/cloudmade_madison_square_garden
new file mode 100644
index 0000000..6fa280d
--- /dev/null
+++ b/test/fixtures/cloudmade_madison_square_garden
@@ -0,0 +1 @@
+{"found": 2, "bounds": [[40.74983, -73.99433], [40.75116, -73.99266]], "features": [{"id": 32891803,"centroid": {"type":"POINT","coordinates":[40.75111, -73.99345]},"bounds": [[40.74983, -73.99433], [40.75116, -73.99266]],"properties": {"osm_element": "way", "sport": "hockey;basketball;lacrosse", "name": "Madison Square Garden", "leisure": "stadium", "osm_id": "24801588"},"location": {"county": "New York", "country": "United States of America", "postcode": "10119", "road": "West 31st Street", "city": "New York"},"type": "Feature"},{"id": 12977552,"centroid": {"type":"POINT","coordinates":[40.75066, -73.99347]},"bounds": [[40.75066, -73.99347], [40.75066, -73.99347]],"properties": {"building": "yes", "osm_element": "node", "name": "Madison Square Garden Center", "addr:state": "NY", "osm_id": "368045579"},"location": {"county": "New York", "country": "United States of America", "postcode": "10119", "road": "West 33rd Street", "city": "New York"},"type": "Feature"}], "type": "FeatureCollection", "crs": {"type": "EPSG", "properties": {"code": 4326, "coordinate_order": [0, 1]}}}
diff --git a/test/fixtures/cloudmade_no_results b/test/fixtures/cloudmade_no_results
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/test/fixtures/cloudmade_no_results
@@ -0,0 +1 @@
+{}
diff --git a/test/fixtures/db_ip_com_invalid_key b/test/fixtures/db_ip_com_invalid_key
new file mode 100644
index 0000000..91cab5e
--- /dev/null
+++ b/test/fixtures/db_ip_com_invalid_key
@@ -0,0 +1 @@
+{"error":"invalid API key"}
\ No newline at end of file
diff --git a/test/fixtures/db_ip_com_madison_square_garden b/test/fixtures/db_ip_com_madison_square_garden
new file mode 100644
index 0000000..a33e2ee
--- /dev/null
+++ b/test/fixtures/db_ip_com_madison_square_garden
@@ -0,0 +1,28 @@
+{
+  "ipAddress": "23.255.240.0",
+  "continentCode": "NA",
+  "continentName": "North America",
+  "countryCode": "US",
+  "countryName": "United States",
+  "currencyCode": "USD",
+  "phonePrefix": "1",
+  "languages": [
+    "en-US",
+    "es-US",
+    "haw",
+    "fr"
+  ],
+  "stateProvCode": "CA",
+  "stateProv": "California",
+  "district": "Santa Clara County",
+  "city": "Mountain View",
+  "geonameId": 5375480,
+  "zipCode": "94043",
+  "latitude": 37.3861,
+  "longitude": -122.084,
+  "gmtOffset": -7,
+  "timeZone": "America\/Los_Angeles",
+  "isp": "Google Fiber Inc.",
+  "linkType": "fttx",
+  "organization": "Google Fiber Inc."
+}
diff --git a/test/fixtures/db_ip_com_no_results b/test/fixtures/db_ip_com_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/db_ip_com_quota_exceeded b/test/fixtures/db_ip_com_quota_exceeded
new file mode 100644
index 0000000..f5cbef5
--- /dev/null
+++ b/test/fixtures/db_ip_com_quota_exceeded
@@ -0,0 +1 @@
+{"error":"maximum number of queries per day exceeded"}
\ No newline at end of file
diff --git a/test/fixtures/db_ip_com_unknown_error b/test/fixtures/db_ip_com_unknown_error
new file mode 100644
index 0000000..7d77aa9
--- /dev/null
+++ b/test/fixtures/db_ip_com_unknown_error
@@ -0,0 +1 @@
+{"error":"unknown error"}
\ No newline at end of file
diff --git a/test/fixtures/esri_austin_tx b/test/fixtures/esri_austin_tx
new file mode 100644
index 0000000..0140001
--- /dev/null
+++ b/test/fixtures/esri_austin_tx
@@ -0,0 +1,64 @@
+{
+ "spatialReference": {
+  "wkid": 4326,
+  "latestWkid": 4326
+ },
+ "locations": [
+  {
+   "name": "Austin, Texas, United States",
+   "extent": {
+    "xmin": -97.935057,
+    "ymin": 30.075146,
+    "xmax": -97.551057,
+    "ymax": 30.459146
+   },
+   "feature": {
+    "geometry": {
+     "x": -97.743055550999657,
+     "y": 30.267145960000448
+    },
+    "attributes": {
+     "Loc_name": "Gaz.WorldGazetteer.POI1",
+     "Score": 100,
+     "Match_addr": "Austin, Texas, United States",
+     "Addr_type": "POI",
+     "Type": "State Capital",
+     "PlaceName": "Austin",
+     "Place_addr": "",
+     "Phone": "",
+     "URL": "",
+     "Rank": "3",
+     "AddBldg": "",
+     "AddNum": "",
+     "AddNumFrom": "",
+     "AddNumTo": "",
+     "Side": "",
+     "StPreDir": "",
+     "StPreType": "",
+     "StName": "",
+     "StType": "",
+     "StDir": "",
+     "StAddr": "",
+     "Nbrhd": "",
+     "City": "",
+     "Subregion": "Travis",
+     "Region": "Texas",
+     "RegionAbbr": "TX",
+     "Postal": "",
+     "PostalExt": "",
+     "Country": "USA",
+     "LangCode": "",
+     "Distance": 0,
+     "X": -97.743056999999993,
+     "Y": 30.267146,
+     "DisplayX": -97.743056999999993,
+     "DisplayY": 30.267146,
+     "Xmin": -97.935057,
+     "Xmax": -97.551057,
+     "Ymin": 30.075146,
+     "Ymax": 30.459146
+    }
+   }
+  }
+ ]
+}
diff --git a/test/fixtures/esri_madison_square_garden b/test/fixtures/esri_madison_square_garden
new file mode 100644
index 0000000..ed015ea
--- /dev/null
+++ b/test/fixtures/esri_madison_square_garden
@@ -0,0 +1,60 @@
+{
+ "spatialReference": {
+  "wkid": 4326,
+  "latestWkid": 4326
+ },
+ "locations": [
+  {
+   "name": "Madison Square Garden",
+   "extent": {
+    "xmin": -74.000241000000003,
+    "ymin": 40.744050000000001,
+    "xmax": -73.988241000000002,
+    "ymax": 40.756050000000002
+   },
+   "feature": {
+    "geometry": {
+     "x": -73.994238897999651,
+     "y": 40.750049813000487
+    },
+    "attributes": {
+     "Loc_name": "Gaz.WorldGazetteer.POI2",
+     "Score": 100,
+     "Match_addr": "Madison Square Garden",
+     "Addr_type": "POI",
+     "Type": "Sports Complex",
+     "PlaceName": "Madison Square Garden",
+     "Rank": "18",
+     "AddBldg": "",
+     "AddNum": "",
+     "AddNumFrom": "",
+     "AddNumTo": "",
+     "Side": "",
+     "StPreDir": "",
+     "StPreType": "",
+     "StName": "",
+     "StType": "",
+     "StDir": "",
+     "Nbrhd": "",
+     "City": "New York",
+     "Subregion": "New York",
+     "Region": "New York",
+     "RegionAbbr": "NY",
+     "Postal": "10001",
+     "PostalExt": "",
+     "Country": "USA",
+     "LangCode": "",
+     "Distance": 0,
+     "X": -73.994240000000005,
+     "Y": 40.750050000000002,
+     "DisplayX": -73.994240000000005,
+     "DisplayY": 40.750050000000002,
+     "Xmin": -74.000241000000003,
+     "Xmax": -73.988241000000002,
+     "Ymin": 40.744050000000001,
+     "Ymax": 40.756050000000002
+    }
+   }
+  }
+ ]
+}
diff --git a/test/fixtures/esri_new_york_ny b/test/fixtures/esri_new_york_ny
new file mode 100644
index 0000000..7ec9f93
--- /dev/null
+++ b/test/fixtures/esri_new_york_ny
@@ -0,0 +1,64 @@
+{
+ "spatialReference": {
+  "wkid": 4326,
+  "latestWkid": 4326
+ },
+ "locations": [
+  {
+   "name": "New York City, New York, United States",
+   "extent": {
+    "xmin": -74.165970999999999,
+    "ymin": 40.554268999999998,
+    "xmax": -73.845971000000006,
+    "ymax": 40.874268999999998
+   },
+   "feature": {
+    "geometry": {
+     "x": -74.005969928999662,
+     "y": 40.714269404000447
+    },
+    "attributes": {
+     "Loc_name": "Gaz.WorldGazetteer.POI1",
+     "Score": 100,
+     "Match_addr": "New York City, New York, United States",
+     "Addr_type": "POI",
+     "Type": "City",
+     "PlaceName": "New York City",
+     "Place_addr": "",
+     "Phone": "",
+     "URL": "",
+     "Rank": "2.5",
+     "AddBldg": "",
+     "AddNum": "",
+     "AddNumFrom": "",
+     "AddNumTo": "",
+     "Side": "",
+     "StPreDir": "",
+     "StPreType": "",
+     "StName": "",
+     "StType": "",
+     "StDir": "",
+     "StAddr": "",
+     "Nbrhd": "",
+     "City": "",
+     "Subregion": "New York",
+     "Region": "New York",
+     "RegionAbbr": "NY",
+     "Postal": "",
+     "PostalExt": "",
+     "Country": "USA",
+     "LangCode": "",
+     "Distance": 0,
+     "X": -74.005971000000002,
+     "Y": 40.714269000000002,
+     "DisplayX": -74.005971000000002,
+     "DisplayY": 40.714269000000002,
+     "Xmin": -74.165970999999999,
+     "Xmax": -73.845971000000006,
+     "Ymin": 40.554268999999998,
+     "Ymax": 40.874268999999998
+    }
+   }
+  }
+ ]
+}
diff --git a/test/fixtures/esri_no_results b/test/fixtures/esri_no_results
new file mode 100644
index 0000000..09cc063
--- /dev/null
+++ b/test/fixtures/esri_no_results
@@ -0,0 +1,8 @@
+{
+ "spatialReference": {
+  "wkid": 4326,
+  "latestWkid": 4326
+ },
+ "locations": [
+  ]
+}
\ No newline at end of file
diff --git a/test/fixtures/esri_reverse b/test/fixtures/esri_reverse
new file mode 100644
index 0000000..00df64c
--- /dev/null
+++ b/test/fixtures/esri_reverse
@@ -0,0 +1,21 @@
+{
+ "address": {
+  "Address": "4 Avenue Gustave Eiffel",
+  "Neighborhood": "7e Arrondissement",
+  "City": "Paris",
+  "Subregion": "Paris",
+  "Region": "Île-de-France",
+  "Postal": "75007",
+  "PostalExt": null,
+  "CountryCode": "FRA",
+  "Loc_name": "FRA.PointAddress"
+ },
+ "location": {
+  "x": 2.2956200048981574,
+  "y": 48.858129997357558,
+  "spatialReference": {
+   "wkid": 4326,
+   "latestWkid": 4326
+  }
+ }
+}
\ No newline at end of file
diff --git a/test/fixtures/esri_washington_dc b/test/fixtures/esri_washington_dc
new file mode 100644
index 0000000..4d8a166
--- /dev/null
+++ b/test/fixtures/esri_washington_dc
@@ -0,0 +1,64 @@
+{
+ "spatialReference": {
+  "wkid": 4326,
+  "latestWkid": 4326
+ },
+ "locations": [
+  {
+   "name": "Washington, D. C., District of Columbia, United States",
+   "extent": {
+    "xmin": -77.176366999999999,
+    "ymin": 38.755108,
+    "xmax": -76.896366999999998,
+    "ymax": 39.035108000000001
+   },
+   "feature": {
+    "geometry": {
+     "x": -77.036365517999627,
+     "y": 38.895107833000452
+    },
+    "attributes": {
+     "Loc_name": "Gaz.WorldGazetteer.POI1",
+     "Score": 100,
+     "Match_addr": "Washington, D. C., District of Columbia, United States",
+     "Addr_type": "POI",
+     "Type": "National Capital",
+     "PlaceName": "Washington",
+     "Place_addr": "",
+     "Phone": "",
+     "URL": "",
+     "Rank": "1.75",
+     "AddBldg": "",
+     "AddNum": "",
+     "AddNumFrom": "",
+     "AddNumTo": "",
+     "Side": "",
+     "StPreDir": "",
+     "StPreType": "",
+     "StName": "",
+     "StType": "",
+     "StDir": "",
+     "StAddr": "",
+     "Nbrhd": "",
+     "City": "",
+     "Subregion": "District of Columbia",
+     "Region": "District of Columbia",
+     "RegionAbbr": "DC",
+     "Postal": "",
+     "PostalExt": "",
+     "Country": "USA",
+     "LangCode": "",
+     "Distance": 0,
+     "X": -77.036366999999998,
+     "Y": 38.895108,
+     "DisplayX": -77.036366999999998,
+     "DisplayY": 38.895108,
+     "Xmin": -77.176366999999999,
+     "Xmax": -76.896366999999998,
+     "Ymin": 38.755108,
+     "Ymax": 39.035108000000001
+    }
+   }
+  }
+ ]
+}
diff --git a/test/fixtures/freegeoip_74_200_247_59 b/test/fixtures/freegeoip_74_200_247_59
new file mode 100644
index 0000000..defc4ac
--- /dev/null
+++ b/test/fixtures/freegeoip_74_200_247_59
@@ -0,0 +1,12 @@
+{
+  "city": "Plano",
+  "region_code": "TX",
+  "region_name": "Texas",
+  "metro_code": "623",
+  "zipcode": "75093",
+  "longitude": -96.8134,
+  "country_name": "United States",
+  "country_code": "US",
+  "ip": "74.200.247.59",
+  "latitude": 33.0347
+}
diff --git a/test/fixtures/freegeoip_74_200_247_60 b/test/fixtures/freegeoip_74_200_247_60
new file mode 100644
index 0000000..77afbe6
--- /dev/null
+++ b/test/fixtures/freegeoip_74_200_247_60
@@ -0,0 +1,12 @@
+{
+  "city": "Nuevo Laredo",
+  "region_code": "TAM",
+  "region_name": "Tamaulipas",
+  "metro_code": "0",
+  "zipcode": "",
+  "country_name": "Mexico",
+  "country_code": "MX",
+  "ip": "74.200.247.60",
+  "latitude": "27.5",
+  "longitude": "-99.517"
+}
diff --git a/test/fixtures/freegeoip_no_results b/test/fixtures/freegeoip_no_results
new file mode 100644
index 0000000..834a5f3
--- /dev/null
+++ b/test/fixtures/freegeoip_no_results
@@ -0,0 +1 @@
+404 page not found
diff --git a/test/fixtures/geoapify_invalid_key b/test/fixtures/geoapify_invalid_key
new file mode 100644
index 0000000..b65614f
--- /dev/null
+++ b/test/fixtures/geoapify_invalid_key
@@ -0,0 +1,5 @@
+{
+  "statusCode":401,
+  "error":"Unauthorized",
+  "message":"Invalid apiKey"
+}
diff --git a/test/fixtures/geoapify_invalid_request b/test/fixtures/geoapify_invalid_request
new file mode 100644
index 0000000..336724e
--- /dev/null
+++ b/test/fixtures/geoapify_invalid_request
@@ -0,0 +1,5 @@
+{
+  "statusCode":500,
+  "error":"Internal Server Error",
+  "message":"Request failed, please try again later"
+}
diff --git a/test/fixtures/geoapify_madison_square_garden b/test/fixtures/geoapify_madison_square_garden
new file mode 100644
index 0000000..6afb01b
--- /dev/null
+++ b/test/fixtures/geoapify_madison_square_garden
@@ -0,0 +1,64 @@
+{
+  "type":"FeatureCollection",
+  "features":[
+    {
+      "type":"Feature",
+      "geometry":{
+        "type":"Point",
+        "coordinates":[
+          -73.993368,
+          40.750487
+        ]
+      },
+      "properties":{
+        "housenumber":"4",
+        "street":"Pennsylvania Plaza",
+        "country":"United States",
+        "county":"New York County",
+        "datasource":{
+          "sourcename":"openstreetmap",
+          "wheelchair":"limited",
+          "wikidata":"Q186125",
+          "wikipedia":"en:Madison Square Garden",
+          "website":"http://www.thegarden.com/",
+          "phone":"12124656741",
+          "osm_type":"W",
+          "osm_id":138141251,
+          "continent":"North America"
+        },
+        "country_code":"us",
+        "postcode":"10001",
+        "state":"New York",
+        "city":"New York",
+        "district":"Manhattan",
+        "suburb":"Chelsea",
+        "lon":-73.993368,
+        "lat":40.750487,
+        "formatted":"4 Pennsylvania Plaza, New York, NY 10001, United States of America",
+        "address_line1":"4 Pennsylvania Plaza",
+        "address_line2":"New York, NY 10001, United States of America",
+        "result_type":"building",
+        "rank":{
+          "popularity":8.615793062435909,
+          "confidence":1,
+          "match_type":"full_match"
+        }
+      },
+      "bbox":[
+        -73.9944446,
+        40.7498531,
+        -73.9925924,
+        40.751161
+      ]
+    }
+  ],
+  "query":{
+    "text":"Madison Square Garden, New York, NY",
+    "parsed":{
+      "house":"madison square garden",
+      "city":"new york",
+      "state":"ny",
+      "expected_type":"amenity"
+    }
+  }
+}
diff --git a/test/fixtures/geoapify_no_results b/test/fixtures/geoapify_no_results
new file mode 100644
index 0000000..b0d703b
--- /dev/null
+++ b/test/fixtures/geoapify_no_results
@@ -0,0 +1,7 @@
+{
+  "type":"FeatureCollection",
+  "features":[],
+  "query":{
+    "text":"thistextcannotbefoundfromthegeoapifyapi"
+  }
+}
diff --git a/test/fixtures/geoapify_reverse b/test/fixtures/geoapify_reverse
new file mode 100644
index 0000000..2cd1223
--- /dev/null
+++ b/test/fixtures/geoapify_reverse
@@ -0,0 +1,55 @@
+{
+  "type":"FeatureCollection",
+  "features":[
+    {
+      "type":"Feature",
+      "geometry":{
+        "type":"Point",
+        "coordinates":[
+          -73.993368,
+          40.750487
+        ]
+      },
+      "properties":{
+        "name":"Madison Square Garden",
+        "housenumber":"4",
+        "street":"Pennsylvania Plaza",
+        "distance":14.791104652930729,
+        "country":"United States",
+        "county":"New York County",
+        "datasource":{
+          "sourcename":"openstreetmap",
+          "wheelchair":"limited",
+          "wikidata":"Q186125",
+          "wikipedia":"en:Madison Square Garden",
+          "website":"http://www.thegarden.com/",
+          "phone":"12124656741",
+          "osm_type":"W",
+          "osm_id":138141251,
+          "continent":"North America"
+        },
+        "country_code":"us",
+        "postcode":"10001",
+        "state":"New York",
+        "city":"New York",
+        "district":"Manhattan",
+        "suburb":"Chelsea",
+        "lon":-73.993368,
+        "lat":40.750487,
+        "result_type":"amenity",
+        "formatted":"Madison Square Garden, 4 Pennsylvania Plaza, New York, NY 10001, United States of America",
+        "address_line1":"Madison Square Garden",
+        "address_line2":"4 Pennsylvania Plaza, New York, NY 10001, United States of America",
+        "rank":{
+          "popularity":8.615793062435909
+        }
+      },
+      "bbox":[
+        -73.9944446,
+        40.7498531,
+        -73.9925924,
+        40.751161
+      ]
+    }
+  ]
+}
diff --git a/test/fixtures/geocoder_ca_madison_square_garden b/test/fixtures/geocoder_ca_madison_square_garden
new file mode 100644
index 0000000..b7bb6b7
--- /dev/null
+++ b/test/fixtures/geocoder_ca_madison_square_garden
@@ -0,0 +1 @@
+test({   "standard" : {      "staddress" : "Madison Square Garden",      "stnumber" : "1",      "prov" : "NY",      "city" : "New York",      "confidence" : "0.9"   },   "longt" : "-73.994240",   "latt" : "40.750050"});
diff --git a/test/fixtures/geocoder_ca_no_results b/test/fixtures/geocoder_ca_no_results
new file mode 100644
index 0000000..e30a039
--- /dev/null
+++ b/test/fixtures/geocoder_ca_no_results
@@ -0,0 +1 @@
+test({"longt":"-0.000000","error":{"description":"Postal Code is not in the proper Format.","code":"005"},"latt":"176946676.000000"});
diff --git a/test/fixtures/geocoder_ca_reverse b/test/fixtures/geocoder_ca_reverse
new file mode 100644
index 0000000..85001ff
--- /dev/null
+++ b/test/fixtures/geocoder_ca_reverse
@@ -0,0 +1,34 @@
+test({
+  "latt":"45.423733",
+  "longt":"-75.676333",
+  "inlatt":"45.424035",
+  "inlongt":"-75.675941",
+  "betweenRoad1":"Chapel",
+  "betweenRoad2":"Russell",
+  "distance":"0.034",
+  "stnumber":"289",
+  "staddress":"Somerset ST E",
+  "city":"Ottawa",
+  "prov":"ON",
+  "postal":"K1N6W1",
+  "NearRoad":"Somerset ST E",
+  "NearRoadDistance":"0.013",
+  "intersection":{
+    "lattx":"45.423733",
+    "longtx":"-75.676333",
+    "distance":"0.045",
+    "street1":"Somerset St E",
+    "street2":"Russell Ave",
+    "city":"OTTAWA",
+    "prov":"ON"
+  },
+  "major_intersection":{
+    "lattx":"45.4241623853",
+    "longtx":"-75.6753026518",
+    "distance":"0.052",
+    "street1":"Chapel St",
+    "street2":"Somerset St E",
+    "city":"OTTAWA",
+    "prov":"ON"
+  }
+});
diff --git a/test/fixtures/geocodio_1101_pennsylvania_ave b/test/fixtures/geocodio_1101_pennsylvania_ave
new file mode 100644
index 0000000..811955c
--- /dev/null
+++ b/test/fixtures/geocodio_1101_pennsylvania_ave
@@ -0,0 +1 @@
+{"input":{"address_components":{"number":"1101","street":"Pennsylvania","suffix":"Ave","postdirectional":"NW","formatted_street":"Pennsylvania Ave NW","city":"Washington","state":"DC","country":"US"},"formatted_address":"1101 Pennsylvania Ave NW, Washington, DC"},"results":[{"address_components":{"number":"1101","street":"Pennsylvania","suffix":"Ave","postdirectional":"NW","formatted_street":"Pennsylvania Ave NW","city":"Washington","county":"District of Columbia","state":"DC","zip":"20004","country":"US"},"formatted_address":"1101 Pennsylvania Ave NW, Washington, DC 20004","location":{"lat":38.895343,"lng":-77.027385},"accuracy":1,"accuracy_type":"rooftop","source":"City of Washington"},{"address_components":{"number":"1101","street":"Pennsylvania","suffix":"Ave","postdirectional":"SE","formatted_street":"Pennsylvania Ave SE","city":"Washington","county":"District of Columbia","state":"DC","zip":"20003","country":"US"},"formatted_address":"1101 Pennsylvania Ave SE, Washington, DC 20003","location":{"lat":38.882434,"lng":-76.991355},"accuracy":0.8,"accuracy_type":"rooftop","source":"City of Washington"},{"address_components":{"number":"1101","street":"15th","suffix":"St","postdirectional":"NW","formatted_street":"15th St NW","city":"Washington","county":"District of Columbia","state":"DC","zip":"20005","country":"US"},"formatted_address":"1101 15th St NW, Washington, DC 20005","location":{"lat":38.903941,"lng":-77.034218},"accuracy":0.5,"accuracy_type":"rooftop","source":"City of Washington"}]}
\ No newline at end of file
diff --git a/test/fixtures/geocodio_bad_api_key b/test/fixtures/geocodio_bad_api_key
new file mode 100644
index 0000000..fb55251
--- /dev/null
+++ b/test/fixtures/geocodio_bad_api_key
@@ -0,0 +1,3 @@
+{
+  "error": "Invalid API key"
+}
diff --git a/test/fixtures/geocodio_invalid b/test/fixtures/geocodio_invalid
new file mode 100644
index 0000000..f716108
--- /dev/null
+++ b/test/fixtures/geocodio_invalid
@@ -0,0 +1,4 @@
+{
+  "error": "Could not geocode address, zip code, city or city/state are required"
+}
+
diff --git a/test/fixtures/geocodio_no_results b/test/fixtures/geocodio_no_results
new file mode 100644
index 0000000..45b14b1
--- /dev/null
+++ b/test/fixtures/geocodio_no_results
@@ -0,0 +1 @@
+{"input":{"address_components":{"number":"1101","street":"Pennsylvania","suffix":"Ave","postdirectional":"NW","city":"Washington","state":"DC"},"formatted_address":"1101 Pennsylvania Ave NW, Washington DC"},"results":[]}
diff --git a/test/fixtures/geocodio_over_query_limit b/test/fixtures/geocodio_over_query_limit
new file mode 100644
index 0000000..803542d
--- /dev/null
+++ b/test/fixtures/geocodio_over_query_limit
@@ -0,0 +1,4 @@
+{
+  "error": "You have reached your daily maximum, please add a payment method for continued use. You can configure billing at https://dash.geocod.io"
+}
+
diff --git a/test/fixtures/geocodio_reverse b/test/fixtures/geocodio_reverse
new file mode 100644
index 0000000..c786289
--- /dev/null
+++ b/test/fixtures/geocodio_reverse
@@ -0,0 +1,2 @@
+
+{"results":[{"address_components":{"number":"483","street":"Bay","suffix":"St","formatted_street":"Bay St","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"483 Bay St, Toronto, ON M5G","location":{"lat":43.652961,"lng":-79.382624},"accuracy":1,"accuracy_type":"nearest_street","source":"CanVec+ by Natural Resources Canada","accuracy_reasons":["Distance from source coordinate: -0.006"]},{"address_components":{"number":"20","street":"Albert","suffix":"St","formatted_street":"Albert St","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"20 Albert St, Toronto, ON M5G","location":{"lat":43.652961,"lng":-79.382624},"accuracy":1,"accuracy_type":"nearest_street","source":"CanVec+ by Natural Resources Canada","accuracy_reasons":["Distance from source coordinate: -0.006"]},{"address_components":{"number":"100","street":"Queen","suffix":"St","postdirectional":"W","formatted_street":"Queen St W","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"100 Queen St W, Toronto, ON M5G","location":{"lat":43.652822,"lng":-79.383625},"accuracy":0.99,"accuracy_type":"rooftop","source":"City of Toronto","accuracy_reasons":["Distance from source coordinate: -0.051"]},{"address_components":{"number":"483","street":"Bay","suffix":"St","formatted_street":"Bay St","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"483 Bay St, Toronto, ON M5G","location":{"lat":43.653759,"lng":-79.38205},"accuracy":0.99,"accuracy_type":"rooftop","source":"City of Toronto","accuracy_reasons":["Distance from source coordinate: -0.062"]},{"address_components":{"number":"20","street":"Albert","suffix":"St","formatted_street":"Albert St","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"20 Albert St, Toronto, ON M5G","location":{"lat":43.653323,"lng":-79.381481},"accuracy":0.99,"accuracy_type":"rooftop","source":"City of Toronto","accuracy_reasons":["Distance from source coordinate: -0.062"]},{"address_components":{"number":"60","street":"Queen","suffix":"St","postdirectional":"W","formatted_street":"Queen St W","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"60 Queen St W, Toronto, ON M5G","location":{"lat":43.652104,"lng":-79.381752},"accuracy":0.99,"accuracy_type":"rooftop","source":"City of Toronto","accuracy_reasons":["Distance from source coordinate: -0.074"]},{"address_components":{"number":"50","street":"Queen","suffix":"St","postdirectional":"W","formatted_street":"Queen St W","city":"Toronto","state":"ON","zip":"M5G","country":"CA"},"formatted_address":"50 Queen St W, Toronto, ON M5G","location":{"lat":43.652209,"lng":-79.381443},"accuracy":0.99,"accuracy_type":"rooftop","source":"City of Toronto","accuracy_reasons":["Distance from source coordinate: -0.079"]}]}
\ No newline at end of file
diff --git a/test/fixtures/geoportail_lu_boulevard_royal b/test/fixtures/geoportail_lu_boulevard_royal
new file mode 100644
index 0000000..d1718d2
--- /dev/null
+++ b/test/fixtures/geoportail_lu_boulevard_royal
@@ -0,0 +1,38 @@
+{
+"count": 1,
+"request": "2 boulevard Royal luxembourg",
+"results": [
+{
+"ratio": 1,
+"name": "2,Boulevard Royal 2449 Luxembourg",
+"easting": 77201.38771169016,
+"address": "2 Boulevard Royal,2449 Luxembourg",
+"geomlonlat": {
+"type": "Point",
+"coordinates": [
+6.12939750216249,
+49.6146720749933
+]
+},
+"geom": {
+"type": "Point",
+"coordinates": [
+77201.38771169016,
+75561.5290000001
+]
+},
+"northing": 75561.5290000001,
+"AddressDetails": {
+"zip": "2449",
+"locality": "Luxembourg",
+"id_caclr_street": "55",
+"street": "Boulevard Royal",
+"postnumber": "2",
+"id_caclr_building": "3186"
+},
+"matching street": "Boulevard Royal",
+"accuracy": 8
+}
+],
+"success": true
+}
diff --git a/test/fixtures/geoportail_lu_no_results b/test/fixtures/geoportail_lu_no_results
new file mode 100644
index 0000000..cef30af
--- /dev/null
+++ b/test/fixtures/geoportail_lu_no_results
@@ -0,0 +1,22 @@
+{
+"count": 1,
+"request": "no results",
+"results": [
+{
+"geom": null,
+"ratio": 0,
+"name": ",  ",
+"address": null,
+"country": null,
+"AddressDetails": {
+"street": null,
+"locality": null,
+"zip": null,
+"postnumber": null
+},
+"matching street": null,
+"accuracy": 1
+}
+],
+"success": false
+}
diff --git a/test/fixtures/geoportail_lu_reverse b/test/fixtures/geoportail_lu_reverse
new file mode 100644
index 0000000..e2a2fc9
--- /dev/null
+++ b/test/fixtures/geoportail_lu_reverse
@@ -0,0 +1,36 @@
+{
+"count": 1,
+"results": [
+{
+"distance": 55.8617929519411,
+"geom": {
+"type": "Point",
+"coordinates": [
+76866.57071788973,
+75026.12585000045
+]
+},
+"name": "39,Boulevard Prince Henri 1724 Luxembourg",
+"easting": 76866.57071788973,
+"address": "39 Boulevard Prince Henri,1724 Luxembourg",
+"geomlonlat": {
+"type": "Point",
+"coordinates": [
+6.12476867352074,
+49.6098566608772
+]
+},
+"AddressDetails": {
+"zip": "1724",
+"locality": "Luxembourg",
+"id_caclr_street": "40",
+"street": "Boulevard Prince Henri",
+"postnumber": "39",
+"id_caclr_building": "2459"
+},
+"matching street": "Boulevard Prince Henri",
+"northing": 75026.12585000045
+}
+],
+"success": true
+}
diff --git a/test/fixtures/google_garbage b/test/fixtures/google_garbage
new file mode 100644
index 0000000..fd728e5
--- /dev/null
+++ b/test/fixtures/google_garbage
@@ -0,0 +1,456 @@
+{
+  "status": "OK",
+  "results": [ {
+    "types": [ "postal_code" ],
+    "formatted_address": "Hedge End, Southampton, Hampshire SO30 4RT, UK",
+    "address_components": [ {
+      "long_name": "SO30 4RT",
+      "short_name": "SO30 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Hedge End",
+      "short_name": "Hedge End",
+      "types": [ "sublocality", "political" ]
+    }, {
+      "long_name": "Southampton",
+      "short_name": "Southampton",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Hampshire",
+      "short_name": "Hampshire",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "SO30",
+      "short_name": "SO30",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 50.9173897,
+        "lng": -1.3125111
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 50.9142421,
+          "lng": -1.3156587
+        },
+        "northeast": {
+          "lat": 50.9205373,
+          "lng": -1.3093635
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Enfield, London N13 4RT, UK",
+    "address_components": [ {
+      "long_name": "N13 4RT",
+      "short_name": "N13 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "London",
+      "short_name": "London",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Enfield",
+      "short_name": "Enfield",
+      "types": [ "administrative_area_level_3", "political" ]
+    }, {
+      "long_name": "Greater London",
+      "short_name": "Greater London",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "N13",
+      "short_name": "N13",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 51.6329009,
+        "lng": -0.0994552
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 51.6297533,
+          "lng": -0.1026028
+        },
+        "northeast": {
+          "lat": 51.6360485,
+          "lng": -0.0963076
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Cardiff, South Glamorgan CF5 4RT, UK",
+    "address_components": [ {
+      "long_name": "CF5 4RT",
+      "short_name": "CF5 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Caerau",
+      "short_name": "Caerau",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Cardiff",
+      "short_name": "Cardiff",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "South Glamorgan",
+      "short_name": "South Glam",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "Cardiff",
+      "short_name": "Cardiff",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "CF5",
+      "short_name": "CF5",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 51.4675314,
+        "lng": -3.2642245
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 51.4643838,
+          "lng": -3.2673721
+        },
+        "northeast": {
+          "lat": 51.4706790,
+          "lng": -3.2610769
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Monifieth, Dundee, Angus DD5 4RT, UK",
+    "address_components": [ {
+      "long_name": "DD5 4RT",
+      "short_name": "DD5 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Monifieth",
+      "short_name": "Monifieth",
+      "types": [ "sublocality", "political" ]
+    }, {
+      "long_name": "Dundee",
+      "short_name": "Dundee",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Angus",
+      "short_name": "Angus",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "DD5",
+      "short_name": "DD5",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 56.4840193,
+        "lng": -2.8291620
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 56.4808717,
+          "lng": -2.8323096
+        },
+        "northeast": {
+          "lat": 56.4871669,
+          "lng": -2.8260144
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Reading RG30 4RT, UK",
+    "address_components": [ {
+      "long_name": "RG30 4RT",
+      "short_name": "RG30 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Reading",
+      "short_name": "Reading",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Reading",
+      "short_name": "Reading",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "RG30",
+      "short_name": "RG30",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 51.4554901,
+        "lng": -1.0393378
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 51.4523425,
+          "lng": -1.0424854
+        },
+        "northeast": {
+          "lat": 51.4586377,
+          "lng": -1.0361902
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Blyth, Northumberland NE24 4RT, UK",
+    "address_components": [ {
+      "long_name": "NE24 4RT",
+      "short_name": "NE24 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Blyth",
+      "short_name": "Blyth",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Northumberland",
+      "short_name": "Nthumb",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "NE24",
+      "short_name": "NE24",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 55.1314523,
+        "lng": -1.5533331
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 55.1283047,
+          "lng": -1.5564807
+        },
+        "northeast": {
+          "lat": 55.1345999,
+          "lng": -1.5501855
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Glasgow, Glasgow City G44 4RT, UK",
+    "address_components": [ {
+      "long_name": "G44 4RT",
+      "short_name": "G44 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Glasgow",
+      "short_name": "Glasgow",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Glasgow City",
+      "short_name": "Glasgow City",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "G44",
+      "short_name": "G44",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 55.8234812,
+        "lng": -4.2507991
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 55.8203336,
+          "lng": -4.2539467
+        },
+        "northeast": {
+          "lat": 55.8266288,
+          "lng": -4.2476515
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Waltham Forest, London E17 4RT, UK",
+    "address_components": [ {
+      "long_name": "E17 4RT",
+      "short_name": "E17 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "London",
+      "short_name": "London",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Walthamstow",
+      "short_name": "Walthamstow",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Waltham Forest",
+      "short_name": "Waltham Forest",
+      "types": [ "administrative_area_level_3", "political" ]
+    }, {
+      "long_name": "Greater London",
+      "short_name": "Greater London",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "E17",
+      "short_name": "E17",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 51.5853714,
+        "lng": -0.0192463
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 51.5822238,
+          "lng": -0.0223939
+        },
+        "northeast": {
+          "lat": 51.5885190,
+          "lng": -0.0160987
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Dunstable, Central Bedfordshire LU5 4RT, UK",
+    "address_components": [ {
+      "long_name": "LU5 4RT",
+      "short_name": "LU5 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Dunstable",
+      "short_name": "Dunstable",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Central Bedfordshire",
+      "short_name": "Central Bedfordshire",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "LU5",
+      "short_name": "LU5",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 51.8869978,
+        "lng": -0.5175666
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 51.8838502,
+          "lng": -0.5207142
+        },
+        "northeast": {
+          "lat": 51.8901454,
+          "lng": -0.5144190
+        }
+      }
+    },
+    "partial_match": true
+  }, {
+    "types": [ "postal_code" ],
+    "formatted_address": "Newton Aycliffe, County Durham DL5 4RT, UK",
+    "address_components": [ {
+      "long_name": "DL5 4RT",
+      "short_name": "DL5 4RT",
+      "types": [ "postal_code" ]
+    }, {
+      "long_name": "Newton Aycliffe",
+      "short_name": "Newton Aycliffe",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Great Aycliffe",
+      "short_name": "Great Aycliffe",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "Durham",
+      "short_name": "Dur",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "United Kingdom",
+      "short_name": "GB",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "DL5",
+      "short_name": "DL5",
+      "types": [ "postal_code_prefix", "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 54.6323273,
+        "lng": -1.5704658
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 54.6291797,
+          "lng": -1.5736134
+        },
+        "northeast": {
+          "lat": 54.6354749,
+          "lng": -1.5673182
+        }
+      }
+    },
+    "partial_match": true
+  } ]
+}
diff --git a/test/fixtures/google_madison_square_garden b/test/fixtures/google_madison_square_garden
new file mode 100644
index 0000000..df3d17b
--- /dev/null
+++ b/test/fixtures/google_madison_square_garden
@@ -0,0 +1,57 @@
+{
+  "status": "OK",
+  "results": [ {
+    "types": [ "street_address" ],
+    "formatted_address": "4 Penn Plaza, New York, NY 10001, USA",
+    "address_components": [ {
+      "long_name": "4",
+      "short_name": "4",
+      "types": [ "street_number" ]
+    }, {
+      "long_name": "Penn Plaza",
+      "short_name": "Penn Plaza",
+      "types": [ "route" ]
+    }, {
+      "long_name": "Manhattan",
+      "short_name": "Manhattan",
+      "types": [ "sublocality", "political" ]
+    }, {
+      "long_name": "New York",
+      "short_name": "New York",
+      "types": [ "locality", "political" ]
+    }, {
+      "long_name": "New York",
+      "short_name": "New York",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "New York",
+      "short_name": "NY",
+      "types": [ "administrative_area_level_1", "political" ]
+    }, {
+      "long_name": "United States",
+      "short_name": "US",
+      "types": [ "country", "political" ]
+    }, {
+      "long_name": "10001",
+      "short_name": "10001",
+      "types": [ "postal_code" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 40.7503540,
+        "lng": -73.9933710
+      },
+      "location_type": "ROOFTOP",
+      "viewport": {
+        "southwest": {
+          "lat": 40.7473324,
+          "lng": -73.9965316
+        },
+        "northeast": {
+          "lat": 40.7536276,
+          "lng": -73.9902364
+        }
+      }
+    }
+  } ]
+}
diff --git a/test/fixtures/google_new_york b/test/fixtures/google_new_york
new file mode 100644
index 0000000..0de608f
--- /dev/null
+++ b/test/fixtures/google_new_york
@@ -0,0 +1,64 @@
+{
+  "status": "OK",
+  "results": [ {
+    "address_components": [
+      {
+        "long_name": "New York",
+        "short_name": "New York",
+        "types": [
+          "locality",
+          "political"
+        ]
+      },
+      {
+        "long_name": "New York",
+        "short_name": "NY",
+        "types": [
+          "administrative_area_level_1",
+          "political"
+        ]
+      },
+      {
+        "long_name": "United States",
+        "short_name": "US",
+        "types": [
+          "country",
+          "political"
+        ]
+      }
+    ],
+    "formatted_address": "New York, NY, USA",
+    "geometry": {
+      "bounds": {
+        "northeast": {
+          "lat": 40.9175771,
+          "lng": -73.7002721
+        },
+        "southwest": {
+          "lat": 40.4773991,
+          "lng": -74.2590899
+        }
+      },
+      "location": {
+        "lat": 40.7127837,
+        "lng": -74.0059413
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "northeast": {
+          "lat": 40.9152555,
+          "lng": -73.7002721
+        },
+        "southwest": {
+          "lat": 40.4960439,
+          "lng": -74.2557349
+        }
+      }
+    },
+    "place_id": "ChIJOwg_06VPwokRYv534QaPC8g",
+    "types": [
+      "locality",
+      "political"
+    ]
+  } ]
+}
diff --git a/test/fixtures/google_no_city_data b/test/fixtures/google_no_city_data
new file mode 100644
index 0000000..72c5280
--- /dev/null
+++ b/test/fixtures/google_no_city_data
@@ -0,0 +1,44 @@
+{
+  "status": "OK",
+  "results": [ {
+    "types": [ "postal_code" ],
+    "formatted_address": "55100, Turkey",
+    "address_components": [{
+      "long_name": "55100",
+      "short_name": "55100",
+      "types": ["postal_code"]
+     },
+     {
+      "long_name": "Turkey",
+      "short_name": "TR",
+      "types": ["country", "political"]
+     }],
+     "geometry": {
+        "location": {
+          "lat": 41.3112221,
+          "lng": 36.3322118
+        },
+        "location_type": "APPROXIMATE",
+        "viewport": {
+          "southwest": {
+              "lat": 41.2933411,
+              "lng": 36.3066331
+          },
+          "northeast": {
+              "lat": 41.3291031,
+              "lng": 36.3577906
+          }
+        },
+        "bounds": {
+          "southwest": {
+              "lat": 41.2933411,
+              "lng": 36.3066331
+          },
+          "northeast": {
+              "lat": 41.3291031,
+              "lng": 36.3577906
+          }
+        }
+      }
+  } ]
+}
\ No newline at end of file
diff --git a/test/fixtures/google_no_locality b/test/fixtures/google_no_locality
new file mode 100644
index 0000000..d012817
--- /dev/null
+++ b/test/fixtures/google_no_locality
@@ -0,0 +1,51 @@
+{
+  "status": "OK",
+  "results": [ {
+    "types": [ "route" ],
+    "formatted_address": "Al Ahram, Haram, Giza, Egypt",
+    "address_components": [ {
+      "long_name": "Al Ahram",
+      "short_name": "Al Ahram",
+      "types": [ "route" ]
+    }, {
+      "long_name": "Haram",
+      "short_name": "Haram",
+      "types": [ "administrative_area_level_2", "political" ]
+    }, {
+      "long_name": "Al Jizah",
+      "short_name": "Al Jizah",
+      "types": [ "administrative_area_level_1", "political" ]
+    }, {
+      "long_name": "Egypt",
+      "short_name": "EG",
+      "types": [ "country", "political" ]
+    } ],
+    "geometry": {
+      "location": {
+        "lat": 29.9803527,
+        "lng": 31.1330307
+      },
+      "location_type": "APPROXIMATE",
+      "viewport": {
+        "southwest": {
+          "lat": 29.9768276,
+          "lng": 31.1302189
+        },
+        "northeast": {
+          "lat": 29.9831228,
+          "lng": 31.1365141
+        }
+      },
+      "bounds": {
+        "southwest": {
+          "lat": 29.9775337,
+          "lng": 31.1327483
+        },
+        "northeast": {
+          "lat": 29.9824167,
+          "lng": 31.1339847
+        }
+      }
+    }
+  } ]
+}
diff --git a/test/fixtures/google_no_results b/test/fixtures/google_no_results
new file mode 100644
index 0000000..afa9ac1
--- /dev/null
+++ b/test/fixtures/google_no_results
@@ -0,0 +1,4 @@
+{
+  "status": "ZERO_RESULTS",
+  "results": [ ]
+}
diff --git a/test/fixtures/google_over_limit b/test/fixtures/google_over_limit
new file mode 100644
index 0000000..353ccf5
--- /dev/null
+++ b/test/fixtures/google_over_limit
@@ -0,0 +1,4 @@
+{
+  "status": "OVER_QUERY_LIMIT",
+  "results": [ ]
+}
diff --git a/test/fixtures/google_places_details_invalid_request b/test/fixtures/google_places_details_invalid_request
new file mode 100644
index 0000000..51fd509
--- /dev/null
+++ b/test/fixtures/google_places_details_invalid_request
@@ -0,0 +1,4 @@
+{
+   "html_attributions" : [],
+   "status" : "INVALID_REQUEST"
+}
diff --git a/test/fixtures/google_places_details_madison_square_garden b/test/fixtures/google_places_details_madison_square_garden
new file mode 100644
index 0000000..1479754
--- /dev/null
+++ b/test/fixtures/google_places_details_madison_square_garden
@@ -0,0 +1,120 @@
+{
+   "html_attributions" : [],
+   "result" : {
+      "address_components" : [
+         {
+            "long_name" : "4",
+            "short_name" : "4",
+            "types" : [ "street_number" ]
+         },
+         {
+            "long_name" : "Pennsylvania Plaza",
+            "short_name" : "Pennsylvania Plaza",
+            "types" : [ "route" ]
+         },
+         {
+            "long_name" : "Chelsea",
+            "short_name" : "Chelsea",
+            "types" : [ "neighborhood", "political" ]
+         },
+         {
+            "long_name" : "Manhattan",
+            "short_name" : "Manhattan",
+            "types" : [ "sublocality_level_1", "sublocality", "political" ]
+         },
+         {
+            "long_name" : "New York",
+            "short_name" : "New York",
+            "types" : [ "locality", "political" ]
+         },
+         {
+            "long_name" : "New York County",
+            "short_name" : "New York County",
+            "types" : [ "administrative_area_level_2", "political" ]
+         },
+         {
+            "long_name" : "NY",
+            "short_name" : "NY",
+            "types" : [ "administrative_area_level_1", "political" ]
+         },
+         {
+            "long_name" : "United States",
+            "short_name" : "US",
+            "types" : [ "country", "political" ]
+         },
+         {
+            "long_name" : "10001",
+            "short_name" : "10001",
+            "types" : [ "postal_code" ]
+         }
+      ],
+      "adr_address" : "\u003cspan class=\"street-address\"\u003e4 Pennsylvania Plaza\u003c/span\u003e, \u003cspan class=\"locality\"\u003eNew York\u003c/span\u003e, \u003cspan class=\"region\"\u003eNY\u003c/span\u003e \u003cspan class=\"postal-code\"\u003e10001\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eUnited States\u003c/span\u003e",
+      "formatted_address" : "4 Pennsylvania Plaza, New York, NY, United States",
+      "formatted_phone_number" : "(212) 465-6741",
+      "geometry" : {
+         "location" : {
+            "lat" : 40.750504,
+            "lng" : -73.993439
+         }
+      },
+      "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/stadium-71.png",
+      "id" : "55e3174d410b31da010030a7dfc0c9819027445a",
+      "international_phone_number" : "+1 212-465-6741",
+      "name" : "Madison Square Garden",
+      "photos" : [
+         {
+            "height" : 267,
+            "html_attributions" : [ "Someone" ],
+            "photo_reference" : "CnRoAAAAB1lz_vOkA1Ffaf7vJ1xcjzVsII-873Z_f8_v3EyW4XbEvlR3VkW_HvNfwF6AwDA0U-ont7fIUqEMyDcovCTWN8RSYN3ibEhqgJPvXxBjeYVi0cc-t-3KfkB_LpV7chPAqhOsdMG56kyzTKIo4lISHxIQqru7QXV6xU11jLaLpi4FNRoUhutg9aq027NghW1d-o9GGE8N3yM",
+            "width" : 400
+         },
+         {
+            "height" : 732,
+            "html_attributions" : [ "Someone else" ],
+            "photo_reference" : "CoQBfwAAADbYj554hM1BgVbBOs6rDjO9UtQ63ecU1P8tI3PfaHame3MB4ipgcNRlv9N2iUa-tXoMq9iXAaDStWgCJ3f48WIuIRbz-Xv-HzSJ0hMCrFiZMRs7kLgaBO7eDJwioTySWcoyAwojretq4mZKF_AcRSUhkqOokBhOstmshWWpSAyyEhAdpL2yp42xDNq2YRiFrx4DGhRZIdt7mochSwZcgSdjESea27Qy9Q",
+            "width" : 929
+         }
+      ],
+      "place_id" : "ChIJhRwB-yFawokR5Phil-QQ3zM",
+      "rating" : 4.4,
+      "reference" : "CnRuAAAArSB-mFxXRjm0ERZIC4c_1gbXwxqmyxUrxws_PQUrz9Y3xZstEwm7_8dLw7zM8hV3vWkPF4RkkZQQ_X01ikDALx2VgV60YwiSX7k3TXxkWnzyFcX0fnHCQLerlYttk_usL7UALQQgMeuf25_eFx3SnhIQxN95Ek3bQVuLVM16yb99ehoU7djcL3cjllohLZ6oJ6X3Tzb9MJQ",
+      "reviews" : [
+         {
+            "aspects" : [
+               {
+                  "rating" : 5,
+                  "type" : "overall"
+               }
+            ],
+            "author_name" : "John Smith",
+            "author_url" : "https://plus.google.com/john.smith",
+            "language" : "en",
+            "rating" : 5,
+            "text" : "It's nice.",
+            "time" : 1407266916
+         },
+         {
+            "aspects" : [
+               {
+                  "rating" : 2,
+                  "type" : "overall"
+               }
+            ],
+            "author_name" : "Jane Smith",
+            "author_url" : "https://plus.google.com/jane.smith",
+            "language" : "en",
+            "rating" : 2,
+            "text" : "Not so nice.",
+            "time" : 1398779079
+         }
+      ],
+      "scope" : "GOOGLE",
+      "types" : [ "stadium", "establishment" ],
+      "url" : "https://plus.google.com/112180896421099179463/about?hl=en-US",
+      "user_ratings_total" : 382,
+      "utc_offset" : -240,
+      "vicinity" : "4 Pennsylvania Plaza, New York",
+      "website" : "http://www.thegarden.com/"
+   },
+   "status" : "OK"
+}
diff --git a/test/fixtures/google_places_details_no_results b/test/fixtures/google_places_details_no_results
new file mode 100644
index 0000000..cc4747f
--- /dev/null
+++ b/test/fixtures/google_places_details_no_results
@@ -0,0 +1,4 @@
+{
+   "html_attributions" : [],
+   "result": {}
+}
diff --git a/test/fixtures/google_places_details_no_reviews b/test/fixtures/google_places_details_no_reviews
new file mode 100644
index 0000000..3d8527b
--- /dev/null
+++ b/test/fixtures/google_places_details_no_reviews
@@ -0,0 +1,60 @@
+{
+   "html_attributions" : [],
+   "result" : {
+      "address_components" : [
+         {
+            "long_name" : "25",
+            "short_name" : "25",
+            "types" : [ "street_number" ]
+         },
+         {
+            "long_name" : "Oranienstraße",
+            "short_name" : "Oranienstraße",
+            "types" : [ "route" ]
+         },
+         {
+            "long_name" : "Friedrichshain-Kreuzberg",
+            "short_name" : "Friedrichshain-Kreuzberg",
+            "types" : [ "sublocality_level_1", "sublocality", "political" ]
+         },
+         {
+            "long_name" : "Berlin",
+            "short_name" : "Berlin",
+            "types" : [ "locality", "political" ]
+         },
+         {
+            "long_name" : "Berlin",
+            "short_name" : "Berlin",
+            "types" : [ "administrative_area_level_1", "political" ]
+         },
+         {
+            "long_name" : "Germany",
+            "short_name" : "DE",
+            "types" : [ "country", "political" ]
+         },
+         {
+            "long_name" : "10999",
+            "short_name" : "10999",
+            "types" : [ "postal_code" ]
+         }
+      ],
+      "adr_address" : "\u003cspan class=\"street-address\"\u003eOranienstraße 25\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e10999\u003c/span\u003e \u003cspan class=\"locality\"\u003eBerlin\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eGermany\u003c/span\u003e",
+      "formatted_address" : "Oranienstraße 25, 10999 Berlin, Germany",
+      "geometry" : {
+         "location" : {
+            "lat" : 52.5010652,
+            "lng" : 13.4206563
+         }
+      },
+      "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png",
+      "id" : "b49e917abd41c247425645a6ac3e5a6756ad80f8",
+      "name" : "Oranienstraße 25",
+      "place_id" : "ChIJQ8-HeTROqEcRGdCTErYy5D0",
+      "reference" : "CpQBjAAAAKrbnaAglhQLI6KPs3EbRp1lVh5UkB4xLEyzOmvvGmtpmwD2EAnlokKcx1VU5PvvB7moRGw6lHTlMTScpGL3GTCC_WM2pzDxgeaAtoB-SR4YQ7PRhHkT2eqxACbaP_70Z9Wyb2J31tG66xBrYASAuBzXgEXYaFpo8dhRBY4xMTfexvMziw6rHs01SxLhCFh-uBIQBQcGVz70_HtaG_g6ZZ5omhoUdIDDSVApRiVoIh61NiCzStYa-x0",
+      "scope" : "GOOGLE",
+      "types" : [ "street_address" ],
+      "url" : "https://maps.google.com/maps/place?q=Oranienstra%C3%9Fe+25,+10999+Berlin,+Germany&ftid=0x47a84e347987cf43:0x3de432b61293d019",
+      "vicinity" : "Friedrichshain-Kreuzberg"
+   },
+   "status" : "OK"
+}
diff --git a/test/fixtures/google_places_details_no_types b/test/fixtures/google_places_details_no_types
new file mode 100644
index 0000000..96903c5
--- /dev/null
+++ b/test/fixtures/google_places_details_no_types
@@ -0,0 +1,66 @@
+{
+   "html_attributions" : [],
+   "result" : {
+      "address_components" : [
+         {
+            "long_name" : "6",
+            "short_name" : "6",
+            "types" : [ "street_number" ]
+         },
+         {
+            "long_name" : "Graniczna",
+            "short_name" : "Graniczna",
+            "types" : [ "route" ]
+         },
+         {
+            "long_name" : "Gdynia",
+            "short_name" : "Gdynia",
+            "types" : [ "locality", "political" ]
+         },
+         {
+            "long_name" : "Gdynia",
+            "short_name" : "Gdynia",
+            "types" : [ "administrative_area_level_3", "political" ]
+         },
+         {
+            "long_name" : "Gdynia",
+            "short_name" : "Gdynia",
+            "types" : [ "administrative_area_level_2", "political" ]
+         },
+         {
+            "long_name" : "Pomeranian Voivodeship",
+            "short_name" : "Pomeranian Voivodeship",
+            "types" : [ "administrative_area_level_1", "political" ]
+         },
+         {
+            "long_name" : "Poland",
+            "short_name" : "PL",
+            "types" : [ "country", "political" ]
+         },
+         {
+            "long_name" : "81-626",
+            "short_name" : "81-626",
+            "types" : [ "postal_code" ]
+         }
+      ],
+      "adr_address" : "\u003cspan class=\"street-address\"\u003eGraniczna 6\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e81-626\u003c/span\u003e \u003cspan class=\"locality\"\u003eGdynia\u003c/span\u003e, \u003cspan class=\"country-name\"\u003ePoland\u003c/span\u003e",
+      "formatted_address" : "Graniczna 6, Gdynia, Poland",
+      "formatted_phone_number" : "795 085 050",
+      "geometry" : {
+         "location" : {
+            "lat" : 54.493608,
+            "lng" : 18.508983
+         }
+      },
+      "id" : "aed47c412bce35bb385c86edef64f8f7860b1cf8",
+      "international_phone_number" : "+48 795 085 050",
+      "name" : "Taxi Gdynia",
+      "place_id" : "ChIJgT2H0tig_UYRD9iM2NSOo4Y",
+      "reference" : "CnRlAAAA6bzgakKCXzwjtLBSMLvj0fvv3OSwGp2WsA54VwQELEupFzqtFuUmxMyMSNYt745EukJR0Ui6Ih9WX3AdL--HXjQprE8xHSb_6qQh2eauKWFIoHzIvbMrkjDIcUPxPDMdJc2XkwpOr_EUimZplCy-gBIQb_UHtRVaswAMjdHHtDqzURoU3tGbILe-zhN3oISBAvc3AyoyznE",
+      "scope" : "GOOGLE",
+      "url" : "https://plus.google.com/100174651414421384172/about?hl=en-US",
+      "utc_offset" : 120,
+      "vicinity" : "Graniczna 6, Gdynia"
+   },
+   "status" : "OK"
+}
diff --git a/test/fixtures/google_places_search_madison_square_garden b/test/fixtures/google_places_search_madison_square_garden
new file mode 100644
index 0000000..38c7114
--- /dev/null
+++ b/test/fixtures/google_places_search_madison_square_garden
@@ -0,0 +1,41 @@
+{
+   "candidates" : [
+      {
+         "formatted_address" : "4 Pennsylvania Plaza, New York, NY 10001, United States",
+         "geometry" : {
+            "location" : {
+               "lat" : 40.75050450000001,
+               "lng" : -73.9934387
+            },
+            "viewport" : {
+               "northeast" : {
+                  "lat" : 40.7523981,
+                  "lng" : -73.98975269999997
+               },
+               "southwest" : {
+                  "lat" : 40.7485721,
+                  "lng" : -73.9963951
+               }
+            }
+         },
+         "icon" : "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png",
+         "id" : "55e3174d410b31da010030a7dfc0c9819027445a",
+         "name" : "Madison Square Garden",
+         "photos" : [
+            {
+               "height" : 3904,
+               "html_attributions" : [
+                  "\u003ca href=\"https://maps.google.com/maps/contrib/117796018192827964147/photos\"\u003eAntonio Vera\u003c/a\u003e"
+               ],
+               "photo_reference" : "CoQBdwAAAJSRc-oTDFp3lkRwkyDN85h49Inw9YRC8U2sPUDeV0FSKzQNMxKfswp27o4Eh8gt1U6ZLPYES3RCP-2zJPswcVMtQOE9NxxM9Yg7SJllFYcC1GR_yjCJw6OGTP3_OCaL-gFI1_r54-04veyCc-UNtzjF836se6yQlSGd643zBy3_EhCYKtc3tY024iyO-6xPZUzzGhRcgC0A-itlFq-U2qmYbPde4gJU7Q",
+               "width" : 3152
+            }
+         ],
+         "place_id" : "ChIJhRwB-yFawokR5Phil-QQ3zM",
+         "rating" : 4.5,
+         "reference" : "CmRRAAAAqb-Y-BFyZh54ipS97aLZCfQYVVI_l87_7HQxJAXMx3rI29XzscexUUiwt7kLbr4YeDaggMQ78coK-V_yGztNvhsGbq2OsrdR-BmVpecrNGbiE9fNDPsPGvdxKcB3SPbAEhDTgnyzjLY1p7IPh2M4L9KnGhS7ZZMAtvVKPnjoCnaWP9IzdzRC4w",
+         "types" : [ "stadium", "point_of_interest", "establishment" ]
+      }
+   ],
+   "status" : "OK"
+}
diff --git a/test/fixtures/google_places_search_no_results b/test/fixtures/google_places_search_no_results
new file mode 100644
index 0000000..6a1ce6a
--- /dev/null
+++ b/test/fixtures/google_places_search_no_results
@@ -0,0 +1,5 @@
+{
+   "html_attributions" : [],
+   "results" : [],
+   "status" : "ZERO_RESULTS"
+}
diff --git a/test/fixtures/here_berlin b/test/fixtures/here_berlin
new file mode 100644
index 0000000..525ff51
--- /dev/null
+++ b/test/fixtures/here_berlin
@@ -0,0 +1,37 @@
+{
+  "items": [
+    {
+      "title": "Berlin, Germany",
+      "id": "here:cm:namedplace:20187403",
+      "resultType": "locality",
+      "localityType": "city",
+      "address": {
+        "label": "Berlin, Germany",
+        "countryCode": "DEU",
+        "countryName": "Germany",
+        "stateCode": "BE",
+        "state": "Berlin",
+        "countyCode": "B",
+        "county": "Berlin",
+        "city": "Berlin",
+        "postalCode": "10117"
+      },
+      "position": {
+        "lat": 52.51604,
+        "lng": 13.37691
+      },
+      "mapView": {
+        "west": 13.08835,
+        "south": 52.33812,
+        "east": 13.761,
+        "north": 52.6755
+      },
+      "scoring": {
+        "queryScore": 1,
+        "fieldScore": {
+          "city": 1
+        }
+      }
+    }
+  ]
+}
diff --git a/test/fixtures/here_madison_square_garden b/test/fixtures/here_madison_square_garden
new file mode 100644
index 0000000..e69d629
--- /dev/null
+++ b/test/fixtures/here_madison_square_garden
@@ -0,0 +1,55 @@
+{
+  "items": [
+    {
+      "title": "Madison Square Garden",
+      "id": "here:pds:place:840dr5ru-7bf91d321e434303aa1094c2c5f1f15f",
+      "resultType": "place",
+      "address": {
+        "label": "Madison Square Garden, 4 Penn Plz, New York, NY 10001, United States",
+        "countryCode": "USA",
+        "countryName": "United States",
+        "stateCode": "NY",
+        "state": "New York",
+        "county": "New York",
+        "city": "New York",
+        "district": "Chelsea",
+        "street": "Penn Plz",
+        "postalCode": "10001",
+        "houseNumber": "4"
+      },
+      "position": {
+        "lat": 40.75051,
+        "lng": -73.9934
+      },
+      "access": [
+        {
+          "lat": 40.75003,
+          "lng": -73.99425
+        }
+      ],
+      "categories": [
+        {
+          "id": "800-8600-0180",
+          "name": "Sports Complex/Stadium",
+          "primary": true
+        },
+        {
+          "id": "200-2200-0020",
+          "name": "Performing Arts Center"
+        },
+        {
+          "id": "700-7400-0141",
+          "name": "Business Services"
+        }
+      ],
+      "scoring": {
+        "queryScore": 1,
+        "fieldScore": {
+          "state": 1,
+          "city": 1,
+          "placeName": 1
+        }
+      }
+    }
+  ]
+}
diff --git a/test/fixtures/here_no_results b/test/fixtures/here_no_results
new file mode 100644
index 0000000..2feb210
--- /dev/null
+++ b/test/fixtures/here_no_results
@@ -0,0 +1,3 @@
+{
+  "items": []
+}
diff --git a/test/fixtures/ip2location_8_8_8_8 b/test/fixtures/ip2location_8_8_8_8
new file mode 100644
index 0000000..84d967d
--- /dev/null
+++ b/test/fixtures/ip2location_8_8_8_8
@@ -0,0 +1,22 @@
+{
+  "country_code":"US",
+  "country_name":"United States",
+  "region_name":"California",
+  "city_name":"Mountain View",
+  "latitude":"37.405992",
+  "longitude":"-122.078515",
+  "zip_code":"94043",
+  "time_zone":"-07:00",
+  "isp":"Google LLC",
+  "domain":"google.com",
+  "net_speed":"T1",
+  "idd_code":"1",
+  "area_code":"650",
+  "weather_station_code":"USCA0746",
+  "weather_station_name":"Mountain View",
+  "mcc":"-",
+  "mnc":"-",
+  "mobile_brand":"-",
+  "elevation":"31",
+  "usage_type":"DCH"
+}
\ No newline at end of file
diff --git a/test/fixtures/ip2location_invalid_api_key b/test/fixtures/ip2location_invalid_api_key
new file mode 100644
index 0000000..698205b
--- /dev/null
+++ b/test/fixtures/ip2location_invalid_api_key
@@ -0,0 +1,3 @@
+{
+  "response":"INVALID ACCOUNT"
+}
\ No newline at end of file
diff --git a/test/fixtures/ip2location_no_results b/test/fixtures/ip2location_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/ipapi_com_74_200_247_59 b/test/fixtures/ipapi_com_74_200_247_59
new file mode 100644
index 0000000..5304304
--- /dev/null
+++ b/test/fixtures/ipapi_com_74_200_247_59
@@ -0,0 +1,19 @@
+{
+  "as": "AS22576 DataPipe, Inc.",
+  "city": "Jersey City",
+  "country": "United States",
+  "countryCode": "US",
+  "isp": "DataPipe",
+  "lat": 40.7209,
+  "lon": -74.0468,
+  "mobile": false,
+  "org": "DataPipe",
+  "proxy": false,
+  "query": "74.200.247.59",
+  "region": "NJ",
+  "regionName": "New Jersey",
+  "reverse": "",
+  "status": "success",
+  "timezone": "America/New_York",
+  "zip": "07302"
+}
diff --git a/test/fixtures/ipapi_com_74_200_247_60 b/test/fixtures/ipapi_com_74_200_247_60
new file mode 100644
index 0000000..b22bbe1
--- /dev/null
+++ b/test/fixtures/ipapi_com_74_200_247_60
@@ -0,0 +1 @@
+invalid key
diff --git a/test/fixtures/ipapi_com_no_results b/test/fixtures/ipapi_com_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/ipbase_74_200_247_59 b/test/fixtures/ipbase_74_200_247_59
new file mode 100644
index 0000000..6021550
--- /dev/null
+++ b/test/fixtures/ipbase_74_200_247_59
@@ -0,0 +1,99 @@
+{
+  "data": {
+    "timezone": {
+      "id": "America/New_York",
+      "current_time": "2022-06-14T11:25:33-04:00",
+      "code": "EDT",
+      "is_daylight_saving": true,
+      "gmt_offset": -14400
+    },
+    "ip": "74.200.247.59",
+    "type": "v4",
+    "connection": {
+      "asn": 19994,
+      "organization": "RACKSPACE",
+      "isp": "Rackspace Hosting"
+    },
+    "location": {
+      "geonames_id": 5099836,
+      "latitude": 40.72010040283203,
+      "longitude": -74.04312896728516,
+      "zip": "07302",
+      "continent": {
+        "code": "NA",
+        "name": "North America",
+        "name_translated": "North America"
+      },
+      "country": {
+        "alpha2": "US",
+        "alpha3": "USA",
+        "calling_codes": [
+          "+1"
+        ],
+        "currencies": [
+          {
+            "symbol": "$",
+            "name": "US Dollar",
+            "symbol_native": "$",
+            "decimal_digits": 2,
+            "rounding": 0,
+            "code": "USD",
+            "name_plural": "US dollars"
+          }
+        ],
+        "emoji": "🇺🇸",
+        "ioc": "USA",
+        "languages": [
+          {
+            "name": "English",
+            "name_native": "English"
+          }
+        ],
+        "name": "United States",
+        "name_translated": "United States",
+        "timezones": [
+          "America/New_York",
+          "America/Detroit",
+          "America/Kentucky/Louisville",
+          "America/Kentucky/Monticello",
+          "America/Indiana/Indianapolis",
+          "America/Indiana/Vincennes",
+          "America/Indiana/Winamac",
+          "America/Indiana/Marengo",
+          "America/Indiana/Petersburg",
+          "America/Indiana/Vevay",
+          "America/Chicago",
+          "America/Indiana/Tell_City",
+          "America/Indiana/Knox",
+          "America/Menominee",
+          "America/North_Dakota/Center",
+          "America/North_Dakota/New_Salem",
+          "America/North_Dakota/Beulah",
+          "America/Denver",
+          "America/Boise",
+          "America/Phoenix",
+          "America/Los_Angeles",
+          "America/Anchorage",
+          "America/Juneau",
+          "America/Sitka",
+          "America/Metlakatla",
+          "America/Yakutat",
+          "America/Nome",
+          "America/Adak",
+          "Pacific/Honolulu"
+        ],
+        "is_in_european_union": false
+      },
+      "city": {
+        "name": "Jersey City",
+        "name_translated": "Jersey City"
+      },
+      "region": {
+        "fips": "",
+        "alpha2": "",
+        "name": "New Jersey",
+        "name_translated": "New Jersey"
+      }
+    }
+  }
+}
diff --git a/test/fixtures/ipbase_invalid_ip b/test/fixtures/ipbase_invalid_ip
new file mode 100644
index 0000000..8387e20
--- /dev/null
+++ b/test/fixtures/ipbase_invalid_ip
@@ -0,0 +1,40 @@
+{
+  "data": {
+    "message": "The ip must be a valid IP address.",
+    "errors": {
+      "ip": [
+        "The ip must be a valid IP address."
+      ]
+    }
+  },
+  "status": 422,
+  "statusText": "",
+  "headers": {
+    "cache-control": "no-cache, private",
+    "content-type": "application/json"
+  },
+  "config": {
+    "transitional": {
+      "silentJSONParsing": true,
+      "forcedJSONParsing": true,
+      "clarifyTimeoutError": false
+    },
+    "transformRequest": [
+      null
+    ],
+    "transformResponse": [
+      null
+    ],
+    "timeout": 0,
+    "xsrfCookieName": "XSRF-TOKEN",
+    "xsrfHeaderName": "X-XSRF-TOKEN",
+    "maxContentLength": -1,
+    "maxBodyLength": -1,
+    "headers": {
+      "Accept": "application/json, text/plain, */*",
+      "X-Requested-With": "XMLHttpRequest"
+    },
+    "method": "get"
+  },
+  "request": {}
+}
diff --git a/test/fixtures/ipbase_no_data b/test/fixtures/ipbase_no_data
new file mode 100644
index 0000000..68921bf
--- /dev/null
+++ b/test/fixtures/ipbase_no_data
@@ -0,0 +1,3 @@
+{
+  "message": "No data found for this IP"
+}
diff --git a/test/fixtures/ipbase_no_results b/test/fixtures/ipbase_no_results
new file mode 100644
index 0000000..14dfc69
--- /dev/null
+++ b/test/fixtures/ipbase_no_results
@@ -0,0 +1,35 @@
+{
+  "data": {
+    "message": "No data found for this IP"
+  },
+  "status": 404,
+  "statusText": "",
+  "headers": {
+    "cache-control": "no-cache, private",
+    "content-type": "application/json; charset=UTF-8"
+  },
+  "config": {
+    "transitional": {
+      "silentJSONParsing": true,
+      "forcedJSONParsing": true,
+      "clarifyTimeoutError": false
+    },
+    "transformRequest": [
+      null
+    ],
+    "transformResponse": [
+      null
+    ],
+    "timeout": 0,
+    "xsrfCookieName": "XSRF-TOKEN",
+    "xsrfHeaderName": "X-XSRF-TOKEN",
+    "maxContentLength": -1,
+    "maxBodyLength": -1,
+    "headers": {
+      "Accept": "application/json, text/plain, */*",
+      "X-Requested-With": "XMLHttpRequest"
+    },
+    "method": "get"
+  },
+  "request": {}
+}
diff --git a/test/fixtures/ipdata_co_74_200_247_59 b/test/fixtures/ipdata_co_74_200_247_59
new file mode 100644
index 0000000..a2da12b
--- /dev/null
+++ b/test/fixtures/ipdata_co_74_200_247_59
@@ -0,0 +1,24 @@
+{
+    "ip": "74.200.247.59",
+    "city": "Jersey City",
+    "region": "New Jersey",
+    "region_code": "NJ",
+    "country_name": "United States",
+    "country_code": "US",
+    "continent_name": "North America",
+    "continent_code": "NA",
+    "latitude": 40.7209,
+    "longitude": -74.0468,
+    "asn": "AS22576",
+    "organisation": "DataPipe, Inc.",
+    "postal": "07302",
+    "currency": "USD",
+    "currency_symbol": "$",
+    "calling_code": "1",
+    "flag": "https://ipdata.co/flags/us.png",
+    "time_zone": "America/New_York",
+    "is_eu": false,
+    "suspicious_factors": {
+        "is_tor": false
+    }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipdata_co_8_8_8 b/test/fixtures/ipdata_co_8_8_8
new file mode 100644
index 0000000..2462e12
--- /dev/null
+++ b/test/fixtures/ipdata_co_8_8_8
@@ -0,0 +1 @@
+8.8.8 does not appear to be an IPv4 or IPv6 address
\ No newline at end of file
diff --git a/test/fixtures/ipdata_co_no_results b/test/fixtures/ipdata_co_no_results
new file mode 100644
index 0000000..896eaf5
--- /dev/null
+++ b/test/fixtures/ipdata_co_no_results
@@ -0,0 +1 @@
+0.0.0 does not appear to be an IPv4 or IPv6 address
\ No newline at end of file
diff --git a/test/fixtures/ipgeolocation_103_217_177_217 b/test/fixtures/ipgeolocation_103_217_177_217
new file mode 100644
index 0000000..d4e6e04
--- /dev/null
+++ b/test/fixtures/ipgeolocation_103_217_177_217
@@ -0,0 +1,37 @@
+{
+    "ip": "103.217.177.217",
+    "continent_code": "AS",
+    "continent_name": "Asia",
+    "country_code2": "PK",
+    "country_code3": "PAK",
+    "country_name": "Pakistan",
+    "country_capital": "Islamabad",
+    "state_prov": "Islamabad",
+    "district": "Islamabad",
+    "city": "Islamabad",
+    "zipcode": "44000",
+    "latitude": "33.7334",
+    "longitude": "73.0785",
+    "is_eu": false,
+    "calling_code": "+92",
+    "country_tld": ".pk",
+    "languages": "ur-PK,en-PK,pa,sd,ps,brh",
+    "country_flag": "https://ipgeolocation.io/static/flags/pk_64.png",
+    "isp": "TES",
+    "connection_type": "",
+    "organization": "Trans World Enterprise Services (Private) Limited",
+    "geoname_id": "1162015",
+    "currency": {
+        "code": "PKR",
+        "name": "Pakistan Rupee",
+        "symbol": "₨"
+    },
+    "time_zone": {
+        "name": "Asia/Karachi",
+        "offset": 5,
+        "current_time": "2019-03-18 19:28:33.837+0500",
+        "current_time_unix": 1552919313.837,
+        "is_dst": false,
+        "dst_savings": 0
+    }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipgeolocation_invalid_api_key b/test/fixtures/ipgeolocation_invalid_api_key
new file mode 100644
index 0000000..09ae15f
--- /dev/null
+++ b/test/fixtures/ipgeolocation_invalid_api_key
@@ -0,0 +1 @@
+{"message":"Provided API key is not valid."}
diff --git a/test/fixtures/ipinfo_io_8_8_8_8 b/test/fixtures/ipinfo_io_8_8_8_8
new file mode 100644
index 0000000..7d21330
--- /dev/null
+++ b/test/fixtures/ipinfo_io_8_8_8_8
@@ -0,0 +1,8 @@
+{
+  "ip": "8.8.8.8",
+  "city": "Mountain View",
+  "region": "California",
+  "country": "US",
+  "loc": "37.3845,-122.0881",
+  "postal": "94040"
+}
diff --git a/test/fixtures/ipinfo_io_no_results b/test/fixtures/ipinfo_io_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/ipqualityscore_74_200_247_59 b/test/fixtures/ipqualityscore_74_200_247_59
new file mode 100644
index 0000000..7c6ae64
--- /dev/null
+++ b/test/fixtures/ipqualityscore_74_200_247_59
@@ -0,0 +1,27 @@
+{
+  "request_id": "3YqddtowOADDvCm",
+  "success": true,
+  "message": "Success",
+  "fraud_score": 78,
+  "country_code": "US",
+  "region": "New Jersey",
+  "city": "Jersey City",
+  "ISP": "Rackspace Hosting",
+  "ASN": 19994,
+  "organization": "Rackspace Hosting",
+  "latitude": 40.73,
+  "longitude": -74.04,
+  "is_crawler": false,
+  "timezone": "America/New_York",
+  "mobile": false,
+  "host": "74.200.247.59",
+  "proxy": true,
+  "vpn": true,
+  "tor": false,
+  "active_vpn": false,
+  "active_tor": false,
+  "recent_abuse": false,
+  "bot_status": false,
+  "connection_type": "Corporate",
+  "abuse_velocity": "low"
+}
diff --git a/test/fixtures/ipqualityscore_insufficient_credits b/test/fixtures/ipqualityscore_insufficient_credits
new file mode 100644
index 0000000..e00d3b7
--- /dev/null
+++ b/test/fixtures/ipqualityscore_insufficient_credits
@@ -0,0 +1,5 @@
+{
+  "success": false,
+  "message": "You have insufficient credits to make this query. Please contact IPQualityScore support if this error persists.",
+  "request_id": "5DqddgrDBo9DWfl"
+}
diff --git a/test/fixtures/ipqualityscore_invalid_api_key b/test/fixtures/ipqualityscore_invalid_api_key
new file mode 100644
index 0000000..f253cd3
--- /dev/null
+++ b/test/fixtures/ipqualityscore_invalid_api_key
@@ -0,0 +1,5 @@
+{
+  "success": false,
+  "message": "Invalid or unauthorized key. Please check the API key and try again.",
+  "request_id": "5DqddgrDBo9DWfl"
+}
diff --git a/test/fixtures/ipqualityscore_invalid_request b/test/fixtures/ipqualityscore_invalid_request
new file mode 100644
index 0000000..ffad00f
--- /dev/null
+++ b/test/fixtures/ipqualityscore_invalid_request
@@ -0,0 +1,5 @@
+{
+  "success": false,
+  "message": "Invalid IPv4 address, IPv6 address or hostname. Please check the IP/Hostname and try again.",
+  "request_id": "5DqddgrDBo9DWfl"
+}
diff --git a/test/fixtures/ipqualityscore_quota_exceeded b/test/fixtures/ipqualityscore_quota_exceeded
new file mode 100644
index 0000000..02cff0c
--- /dev/null
+++ b/test/fixtures/ipqualityscore_quota_exceeded
@@ -0,0 +1,5 @@
+{
+  "success": false,
+  "message": "You have exceeded your request quota of 200 per day. Please upgrade to increase your request quota.",
+  "request_id": "5DqddgrDBo9DWfl"
+}
diff --git a/test/fixtures/ipregistry_8_8_8_8 b/test/fixtures/ipregistry_8_8_8_8
new file mode 100644
index 0000000..2842262
--- /dev/null
+++ b/test/fixtures/ipregistry_8_8_8_8
@@ -0,0 +1,108 @@
+{
+  "ip" : "8.8.8.8",
+  "type" : "IPv4",
+  "hostname" : null,
+  "carrier" : {
+    "name" : null,
+    "mcc" : null,
+    "mnc" : null
+  },
+  "connection" : {
+    "asn" : 15169,
+    "domain" : "google.com",
+    "organization" : "Google LLC",
+    "type" : "hosting"
+  },
+  "currency" : {
+    "code" : "USD",
+    "name" : "US Dollar",
+    "plural" : "US dollars",
+    "symbol" : "$",
+    "symbol_native" : "$",
+    "format" : {
+      "negative" : {
+        "prefix" : "-$",
+        "suffix" : ""
+      },
+      "positive" : {
+        "prefix" : "$",
+        "suffix" : ""
+      }
+    }
+  },
+  "location" : {
+    "continent" : {
+      "code" : "NA",
+      "name" : "North America"
+    },
+    "country" : {
+      "area" : 9629091.0,
+      "borders" : [ "CA", "MX" ],
+      "calling_code" : "1",
+      "capital" : "Washington D.C.",
+      "code" : "US",
+      "name" : "United States",
+      "population" : 327167434,
+      "population_density" : 33.98,
+      "flag" : {
+        "emoji" : "🇺🇸",
+        "emoji_unicode" : "U+1F1FA U+1F1F8",
+        "emojitwo" : "https://cdn.ipregistry.co/flags/emojitwo/us.svg",
+        "noto" : "https://cdn.ipregistry.co/flags/noto/us.png",
+        "twemoji" : "https://cdn.ipregistry.co/flags/twemoji/us.svg",
+        "wikimedia" : "https://cdn.ipregistry.co/flags/wikimedia/us.svg"
+      },
+      "languages" : [ {
+        "code" : "en",
+        "name" : "English",
+        "native" : "English"
+      }, {
+        "code" : "es",
+        "name" : "Spanish",
+        "native" : "español"
+      }, {
+        "code" : "haw",
+        "name" : "Hawaiian",
+        "native" : "ʻŌlelo Hawaiʻi"
+      }, {
+        "code" : "fr",
+        "name" : "French",
+        "native" : "français"
+      } ],
+      "tld" : ".us"
+    },
+    "region" : {
+      "code" : "CA",
+      "name" : "California"
+    },
+    "city" : "Mountain View",
+    "postal" : "94043",
+    "latitude" : 37.405992,
+    "longitude" : -122.078515,
+    "language" : {
+      "code" : "en",
+      "name" : "English",
+      "native" : "English"
+    },
+    "in_eu" : false
+  },
+  "security" : {
+    "is_bogon" : false,
+    "is_cloud_provider" : true,
+    "is_tor" : false,
+    "is_tor_exit" : false,
+    "is_proxy" : false,
+    "is_anonymous" : false,
+    "is_abuser" : false,
+    "is_attacker" : false,
+    "is_threat" : true
+  },
+  "time_zone" : {
+    "id" : "America/Los_Angeles",
+    "abbreviation" : "PST",
+    "current_time" : "2019-08-18T22:09:10-07:00",
+    "name" : "Pacific Standard Time",
+    "offset" : -25200,
+    "in_daylight_saving" : true
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipregistry_no_results b/test/fixtures/ipregistry_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/ipstack_134_201_250_155 b/test/fixtures/ipstack_134_201_250_155
new file mode 100644
index 0000000..cff0568
--- /dev/null
+++ b/test/fixtures/ipstack_134_201_250_155
@@ -0,0 +1,49 @@
+{
+  "ip": "134.201.250.155",
+  "hostname": "134.201.250.155",
+  "type": "ipv4",
+  "continent_code": "NA",
+  "continent_name": "North America",
+  "country_code": "US",
+  "country_name": "United States",
+  "region_code": "CA",
+  "region_name": "California",
+  "city": "Los Angeles",
+  "zip": "90013",
+  "latitude": 34.0453,
+  "longitude": -118.2413,
+  "location": {
+    "geoname_id": 5368361,
+    "capital": "Washington D.C.",
+    "languages": [
+        {
+          "code": "en",
+          "name": "English",
+          "native": "English"
+        }
+    ],
+    "country_flag": "https://assets.ipstack.com/images/assets/flags_svg/us.svg",
+    "country_flag_emoji": "🇺🇸",
+    "country_flag_emoji_unicode": "U+1F1FA U+1F1F8",
+    "calling_code": "1",
+    "is_eu": false
+  },
+  "time_zone": {
+    "id": "America/Los_Angeles",
+    "current_time": "2018-03-29T07:35:08-07:00",
+    "gmt_offset": -25200,
+    "code": "PDT",
+    "is_daylight_saving": true
+  },
+  "currency": {
+    "code": "USD",
+    "name": "US Dollar",
+    "plural": "US dollars",
+    "symbol": "$",
+    "symbol_native": "$"
+  },
+  "connection": {
+    "asn": 25876,
+    "isp": "Los Angeles Department of Water & Power"
+  }
+}
diff --git a/test/fixtures/ipstack_access_restricted b/test/fixtures/ipstack_access_restricted
new file mode 100644
index 0000000..652e1cb
--- /dev/null
+++ b/test/fixtures/ipstack_access_restricted
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 105,
+    "type": "function_access_restricted",
+    "info": "The current subscription plan does not support this API endpoint."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_batch_not_supported b/test/fixtures/ipstack_batch_not_supported
new file mode 100644
index 0000000..b60a245
--- /dev/null
+++ b/test/fixtures/ipstack_batch_not_supported
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 303,
+    "type": "batch_not_supported_on_plan",
+    "info": "The Bulk Lookup Endpoint is not supported on the current subscription plan"
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_inactive_user b/test/fixtures/ipstack_inactive_user
new file mode 100644
index 0000000..1ead8fa
--- /dev/null
+++ b/test/fixtures/ipstack_inactive_user
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 102,
+    "type": "inactive_user",
+    "info": "The current user account is not active. User will be prompted to get in touch with Customer Support."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_invalid_access_key b/test/fixtures/ipstack_invalid_access_key
new file mode 100644
index 0000000..f119c85
--- /dev/null
+++ b/test/fixtures/ipstack_invalid_access_key
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 101,
+    "type": "invalid_access_key",
+    "info": "No API Key was specified or an invalid API Key was specified."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_invalid_api_function b/test/fixtures/ipstack_invalid_api_function
new file mode 100644
index 0000000..e580005
--- /dev/null
+++ b/test/fixtures/ipstack_invalid_api_function
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 103,
+    "type": "invalid_api_function",
+    "info": "The requested API endpoint does not exist."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_invalid_fields b/test/fixtures/ipstack_invalid_fields
new file mode 100644
index 0000000..105f541
--- /dev/null
+++ b/test/fixtures/ipstack_invalid_fields
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 301,
+    "type": "invalid_fields",
+    "info": "One or more invalid fields were specified using the fields parameter."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_missing_access_key b/test/fixtures/ipstack_missing_access_key
new file mode 100644
index 0000000..358e5c8
--- /dev/null
+++ b/test/fixtures/ipstack_missing_access_key
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 101,
+    "type": "missing_access_key",
+    "info": "No API Key was specified."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_no_results b/test/fixtures/ipstack_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/ipstack_not_found b/test/fixtures/ipstack_not_found
new file mode 100644
index 0000000..f644fa3
--- /dev/null
+++ b/test/fixtures/ipstack_not_found
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 404,
+    "type": "404_not_found",
+    "info": "The requested resource does not exist."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_protocol_access_restricted b/test/fixtures/ipstack_protocol_access_restricted
new file mode 100644
index 0000000..7c915af
--- /dev/null
+++ b/test/fixtures/ipstack_protocol_access_restricted
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 105,
+    "type": "https_access_restricted",
+    "info": "The user's current subscription plan does not support HTTPS Encryption."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_too_many_ips b/test/fixtures/ipstack_too_many_ips
new file mode 100644
index 0000000..b85c5e5
--- /dev/null
+++ b/test/fixtures/ipstack_too_many_ips
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 302,
+    "type": "too_many_ips",
+    "info": "Too many IPs have been specified for the Bulk Lookup Endpoint. (max. 50)"
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipstack_usage_limit b/test/fixtures/ipstack_usage_limit
new file mode 100644
index 0000000..d3753f0
--- /dev/null
+++ b/test/fixtures/ipstack_usage_limit
@@ -0,0 +1,8 @@
+{
+  "success": false,
+  "error": {
+    "code": 104,
+    "type": "usage_limit_reached",
+    "info": "The maximum allowed amount of monthly API requests has been reached."
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/latlon_6000_universal_blvd b/test/fixtures/latlon_6000_universal_blvd
new file mode 100644
index 0000000..97baa14
--- /dev/null
+++ b/test/fixtures/latlon_6000_universal_blvd
@@ -0,0 +1,16 @@
+{
+   "lat": 28.4750507575094,
+   "lon": -81.4630386931719,
+   "address": {
+      "address": "6000 Universal Blvd, Orlando, FL 32819",
+      "prefix": null,
+      "number": "6000",
+      "street_name": "Universal",
+      "street_type": "Blvd",
+      "suffix": null,
+      "unit": null,
+      "city": "Orlando",
+      "state": "FL",
+      "zip": "32819"
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/latlon_invalid_key b/test/fixtures/latlon_invalid_key
new file mode 100644
index 0000000..d00ea46
--- /dev/null
+++ b/test/fixtures/latlon_invalid_key
@@ -0,0 +1,6 @@
+{
+   "error": {
+      "type": "authentication_error",
+      "message": "Failed to authenticate your request."
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/latlon_no_results b/test/fixtures/latlon_no_results
new file mode 100644
index 0000000..f0d0579
--- /dev/null
+++ b/test/fixtures/latlon_no_results
@@ -0,0 +1,6 @@
+{
+   "error": {
+      "type": "api_error",
+      "message": "The requested resource could not be found."
+   }
+}
diff --git a/test/fixtures/location_iq_invalid_api_key b/test/fixtures/location_iq_invalid_api_key
new file mode 100644
index 0000000..636cff2
--- /dev/null
+++ b/test/fixtures/location_iq_invalid_api_key
@@ -0,0 +1,3 @@
+{
+    "error": "Invalid key"
+}
diff --git a/test/fixtures/location_iq_invalid_request b/test/fixtures/location_iq_invalid_request
new file mode 100644
index 0000000..41ffc4c
--- /dev/null
+++ b/test/fixtures/location_iq_invalid_request
@@ -0,0 +1,3 @@
+{
+    "error": "Unknown error - Please try again after some time"
+}
diff --git a/test/fixtures/location_iq_madison_square_garden b/test/fixtures/location_iq_madison_square_garden
new file mode 100644
index 0000000..b1c03ff
--- /dev/null
+++ b/test/fixtures/location_iq_madison_square_garden
@@ -0,0 +1,150 @@
+[
+
+    {
+        "place_id": "30632629",
+        "licence": "Data Copyright OpenStreetMap Contributors, Some Rights Reserved. CC-BY-SA 2.0.",
+        "osm_type": "way",
+        "osm_id": "24801588",
+        "boundingbox": [
+            "40.749828338623",
+            "40.7511596679688",
+            "-73.9943389892578",
+            "-73.9926528930664"
+        ],
+        "polygonpoints": [
+            [
+                "-73.9943346",
+                "40.7503638"
+            ],
+            [
+                "-73.9942745",
+                "40.7504158"
+            ],
+            [
+                "-73.9942593",
+                "40.750629"
+            ],
+            [
+                "-73.9941343",
+                "40.7508432"
+            ],
+            [
+                "-73.9939794",
+                "40.7509703"
+            ],
+            [
+                "-73.9938042",
+                "40.7510532"
+            ],
+            [
+                "-73.9938025",
+                "40.7511311"
+            ],
+            [
+                "-73.9936051",
+                "40.7511571"
+            ],
+            [
+                "-73.9935673",
+                "40.751105"
+            ],
+            [
+                "-73.9934095",
+                "40.7511089"
+            ],
+            [
+                "-73.9931235",
+                "40.7510548"
+            ],
+            [
+                "-73.9928863",
+                "40.7509311"
+            ],
+            [
+                "-73.9928068",
+                "40.750949"
+            ],
+            [
+                "-73.992721",
+                "40.7508515"
+            ],
+            [
+                "-73.9927444",
+                "40.7507889"
+            ],
+            [
+                "-73.9926693",
+                "40.7506457"
+            ],
+            [
+                "-73.9926597",
+                "40.7503657"
+            ],
+            [
+                "-73.9928305",
+                "40.7500953"
+            ],
+            [
+                "-73.9929757",
+                "40.7499911"
+            ],
+            [
+                "-73.9931281",
+                "40.7499238"
+            ],
+            [
+                "-73.993133",
+                "40.7498631"
+            ],
+            [
+                "-73.9932961",
+                "40.7498306"
+            ],
+            [
+                "-73.9933664",
+                "40.7498742"
+            ],
+            [
+                "-73.993471",
+                "40.7498701"
+            ],
+            [
+                "-73.9938023",
+                "40.7499263"
+            ],
+            [
+                "-73.9940703",
+                "40.7500756"
+            ],
+            [
+                "-73.9941876",
+                "40.7502038"
+            ],
+            [
+                "-73.9942831",
+                "40.7502142"
+            ],
+            [
+                "-73.9943346",
+                "40.7503638"
+            ]
+        ],
+        "lat": "40.7504928941818",
+        "lon": "-73.993466492276",
+        "display_name": "Madison Square Garden, West 31st Street, Long Island City, New York City, New York, 10001, United States of America",
+        "class": "leisure",
+        "type": "stadium",
+        "address": {
+            "stadium": "Madison Square Garden",
+            "road": "West 31st Street",
+            "suburb": "Long Island City",
+            "city": "New York City",
+            "county": "New York",
+            "state": "New York",
+            "postcode": "10001",
+            "country": "United States of America",
+            "country_code": "us"
+        }
+    }
+
+]
diff --git a/test/fixtures/location_iq_no_results b/test/fixtures/location_iq_no_results
new file mode 100644
index 0000000..1e3ec72
--- /dev/null
+++ b/test/fixtures/location_iq_no_results
@@ -0,0 +1 @@
+[ ]
diff --git a/test/fixtures/location_iq_over_limit b/test/fixtures/location_iq_over_limit
new file mode 100644
index 0000000..1e6047c
--- /dev/null
+++ b/test/fixtures/location_iq_over_limit
@@ -0,0 +1,3 @@
+{
+    "error": "Rate Limited"
+}
diff --git a/test/fixtures/location_iq_request_denied b/test/fixtures/location_iq_request_denied
new file mode 100644
index 0000000..c3cafe8
--- /dev/null
+++ b/test/fixtures/location_iq_request_denied
@@ -0,0 +1,3 @@
+{
+    "error": "Key not active - Please write to contact@unwiredlabs.com"
+}
diff --git a/test/fixtures/mapbox_Shanghai,_China b/test/fixtures/mapbox_Shanghai,_China
new file mode 100644
index 0000000..f40b8cc
--- /dev/null
+++ b/test/fixtures/mapbox_Shanghai,_China
@@ -0,0 +1,224 @@
+{
+    "type": "FeatureCollection",
+        "query": [
+            "shanghai",
+        "china"
+        ],
+        "features": [
+        {
+            "id": "country.6702069377157440",
+            "type": "Feature",
+            "text": "China",
+            "place_name": "China",
+            "relevance": 0.49,
+            "properties": {
+                "wikidata": "Q148",
+                "short_code": "cn"
+            },
+            "bbox": [
+                73.499857014,
+            15.6816898058459,
+            134.772810006,
+            53.560710919
+            ],
+            "center": [
+                101.901875,
+            35.486703
+            ],
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    101.901875,
+                35.486703
+                ]
+            }
+        },
+        {
+            "id": "neighborhood.8146618069770140",
+            "type": "Feature",
+            "text": "Chinatown",
+            "place_name": "Chinatown, Washington, 20001, District of Columbia, United States",
+            "relevance": 0.49,
+            "properties": {
+
+            },
+            "bbox": [
+                -77.0265197753999,
+            38.8958426648877,
+            -77.0178665827466,
+            38.9031438069192
+            ],
+            "center": [
+                -77.021,
+            38.8998
+            ],
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -77.021,
+                38.8998
+                ]
+            },
+            "context": [
+            {
+                "id": "place.12334081418246050",
+                "text": "Washington",
+                "wikidata": "Q61"
+            },
+            {
+                "id": "postcode.3526019892841050",
+                "text": "20001"
+            },
+            {
+                "id": "region.6884744206035790",
+                "text": "District of Columbia",
+                "short_code": "US-DC",
+                "wikidata": "Q61"
+            },
+            {
+                "id": "country.12862386939497690",
+                "text": "United States",
+                "short_code": "us",
+                "wikidata": "Q30"
+            }
+            ]
+        },
+        {
+            "id": "neighborhood.8687109740770140",
+            "type": "Feature",
+            "text": "Chinatown",
+            "place_name": "Chinatown, San Francisco, 94108, California, United States",
+            "relevance": 0.49,
+            "properties": {
+
+            },
+            "bbox": [
+                -122.411564112065,
+            37.7895769681609,
+            -122.404174805,
+            37.7985673428131
+            ],
+            "center": [
+                -122.4071,
+            37.7943
+            ],
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -122.4071,
+                37.7943
+                ]
+            },
+            "context": [
+            {
+                "id": "place.17925508508361910",
+                "text": "San Francisco",
+                "wikidata": "Q62"
+            },
+            {
+                "id": "postcode.1814482917195520",
+                "text": "94108"
+            },
+            {
+                "id": "region.6020809690311220",
+                "text": "California",
+                "short_code": "US-CA",
+                "wikidata": "Q99"
+            },
+            {
+                "id": "country.12862386939497690",
+                "text": "United States",
+                "short_code": "us",
+                "wikidata": "Q30"
+            }
+            ]
+        },
+        {
+            "id": "region.7004037305164860",
+            "type": "Feature",
+            "text": "Chinandega",
+            "place_name": "Chinandega, Nicaragua",
+            "relevance": 0.49,
+            "properties": {
+                "short_code": "NI-CI",
+                "wikidata": "Q644024"
+            },
+            "bbox": [
+                -87.785656816,
+            12.423387369,
+            -86.654601813223,
+            13.293349936
+            ],
+            "center": [
+                -87.210612,
+            12.857407
+            ],
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -87.210612,
+                12.857407
+                ]
+            },
+            "context": [
+            {
+                "id": "country.11224608483080220",
+                "text": "Nicaragua",
+                "short_code": "ni",
+                "wikidata": "Q811"
+            }
+            ]
+        },
+        {
+            "id": "neighborhood.5866411034770140",
+            "type": "Feature",
+            "text": "Chinatown",
+            "place_name": "Chinatown, Portland, 97209, Oregon, United States",
+            "relevance": 0.49,
+            "properties": {
+
+            },
+            "bbox": [
+                -122.679673745277,
+            45.522452158706,
+            -122.664221513386,
+            45.530089952328
+            ],
+            "center": [
+                -122.673,
+            45.5257
+            ],
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -122.673,
+                45.5257
+                ]
+            },
+            "context": [
+            {
+                "id": "place.10101300842326360",
+                "text": "Portland",
+                "wikidata": "Q6106"
+            },
+            {
+                "id": "postcode.16394767032837600",
+                "text": "97209"
+            },
+            {
+                "id": "region.14077928225490090",
+                "text": "Oregon",
+                "short_code": "US-OR",
+                "wikidata": "Q824"
+            },
+            {
+                "id": "country.12862386939497690",
+                "text": "United States",
+                "short_code": "us",
+                "wikidata": "Q30"
+            }
+            ]
+        }
+        ],
+        "attribution": "© 2016 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service. (https://www.mapbox.com/about/maps/)"
+}
diff --git a/test/fixtures/mapbox_invalid_api_key b/test/fixtures/mapbox_invalid_api_key
new file mode 100644
index 0000000..41a8d6f
--- /dev/null
+++ b/test/fixtures/mapbox_invalid_api_key
@@ -0,0 +1,3 @@
+{
+    "message": "Not Authorized - Invalid Token"
+}
diff --git a/test/fixtures/mapbox_madison_square_garden b/test/fixtures/mapbox_madison_square_garden
new file mode 100644
index 0000000..5860a63
--- /dev/null
+++ b/test/fixtures/mapbox_madison_square_garden
@@ -0,0 +1,175 @@
+{
+	"type": "FeatureCollection",
+	"query": ["madison", "square", "garden", "new", "york", "ny"],
+	"features": [{
+		"id": "poi.77309470334",
+		"type": "Feature",
+		"place_type": ["poi"],
+		"relevance": 0.843333,
+		"properties": {
+			"address": "4 Penn Plz",
+			"foursquare": "4ae6363ef964a520aba521e3",
+			"wikidata": "Q186125",
+			"landmark": true,
+			"category": "outdoors, attraction, tourism"
+		},
+		"text": "Madison Square Garden",
+		"place_name": "Madison Square Garden, 4 Penn Plz, New York, New York 10119, United States",
+		"matching_place_name": "Madison Square Garden, 4 Penn Plz, NY, New York 10119, United States",
+		"center": [-73.993710125, 40.750755],
+		"geometry": {
+			"coordinates": [-73.993710125, 40.750755],
+			"type": "Point"
+		},
+		"context": [{
+			"id": "neighborhood.290417",
+			"text": "Garment District"
+		}, {
+			"id": "postcode.14586990685738040",
+			"text": "10119"
+		}, {
+			"id": "locality.12696928000137850",
+			"wikidata": "Q11299",
+			"text": "Manhattan"
+		}, {
+			"id": "place.2618194975964500",
+			"wikidata": "Q60",
+			"text": "New York"
+		}, {
+			"id": "district.12113562209855570",
+			"wikidata": "Q500416",
+			"text": "New York County"
+		}, {
+			"id": "region.17349986251855570",
+			"wikidata": "Q1384",
+			"short_code": "US-NY",
+			"text": "New York"
+		}, {
+			"id": "country.19678805456372290",
+			"wikidata": "Q30",
+			"short_code": "us",
+			"text": "United States"
+		}]
+	}, {
+		"id": "place.2614879333515050",
+		"type": "Feature",
+		"place_type": ["place"],
+		"relevance": 0.694259,
+		"properties": {
+			"wikidata": "Q938359"
+		},
+		"text": "Madison",
+		"place_name": "Madison, New Jersey, United States",
+		"bbox": [-74.4506873603844, 40.7398880057885, -74.3920610022304, 40.7915999921449],
+		"center": [-74.4171, 40.7598],
+		"geometry": {
+			"type": "Point",
+			"coordinates": [-74.4171, 40.7598]
+		},
+		"context": [{
+			"id": "district.15641134561761970",
+			"wikidata": "Q498163",
+			"text": "Morris County"
+		}, {
+			"id": "region.8524001885700330",
+			"wikidata": "Q1408",
+			"short_code": "US-NJ",
+			"text": "New Jersey"
+		}, {
+			"id": "country.19678805456372290",
+			"wikidata": "Q30",
+			"short_code": "us",
+			"text": "United States"
+		}]
+	}, {
+		"id": "place.2618194975964500",
+		"type": "Feature",
+		"place_type": ["place"],
+		"relevance": 0.592593,
+		"properties": {
+			"wikidata": "Q60"
+		},
+		"text": "New York",
+		"place_name": "New York, New York, United States",
+		"bbox": [-74.25909, 40.477399, -73.700272, 40.917577],
+		"center": [-73.9866, 40.7306],
+		"geometry": {
+			"type": "Point",
+			"coordinates": [-73.9866, 40.7306]
+		},
+		"context": [{
+			"id": "district.12113562209855570",
+			"wikidata": "Q500416",
+			"text": "New York County"
+		}, {
+			"id": "region.17349986251855570",
+			"wikidata": "Q1384",
+			"short_code": "US-NY",
+			"text": "New York"
+		}, {
+			"id": "country.19678805456372290",
+			"wikidata": "Q30",
+			"short_code": "us",
+			"text": "United States"
+		}]
+	}, {
+		"id": "place.11296405918515050",
+		"type": "Feature",
+		"place_type": ["place"],
+		"relevance": 0.592593,
+		"properties": {
+			"wikidata": "Q9026711"
+		},
+		"text": "Madison",
+		"place_name": "Madison, New York, United States",
+		"bbox": [-75.566513439329, 42.8550480032143, -75.4371652073634, 42.9563169973776],
+		"center": [-75.5121, 42.899],
+		"geometry": {
+			"type": "Point",
+			"coordinates": [-75.5121, 42.899]
+		},
+		"context": [{
+			"id": "district.13305009064515050",
+			"wikidata": "Q115166",
+			"text": "Madison County"
+		}, {
+			"id": "region.17349986251855570",
+			"wikidata": "Q1384",
+			"short_code": "US-NY",
+			"text": "New York"
+		}, {
+			"id": "country.19678805456372290",
+			"wikidata": "Q30",
+			"short_code": "us",
+			"text": "United States"
+		}]
+	}, {
+		"id": "district.12113562209855570",
+		"type": "Feature",
+		"place_type": ["district"],
+		"relevance": 0.592593,
+		"properties": {
+			"wikidata": "Q500416"
+		},
+		"text": "New York County",
+		"place_name": "New York County, New York, United States",
+		"bbox": [-74.047227, 40.682932, -73.907, 40.879278],
+		"center": [-74, 40.7167],
+		"geometry": {
+			"type": "Point",
+			"coordinates": [-74, 40.7167]
+		},
+		"context": [{
+			"id": "region.17349986251855570",
+			"wikidata": "Q1384",
+			"short_code": "US-NY",
+			"text": "New York"
+		}, {
+			"id": "country.19678805456372290",
+			"wikidata": "Q30",
+			"short_code": "us",
+			"text": "United States"
+		}]
+	}],
+	"attribution": "NOTICE: © 2021 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained. POI(s) provided by Foursquare."
+}
diff --git a/test/fixtures/mapbox_no_results b/test/fixtures/mapbox_no_results
new file mode 100644
index 0000000..ff814f9
--- /dev/null
+++ b/test/fixtures/mapbox_no_results
@@ -0,0 +1,8 @@
+{
+    "attribution": "NOTICE: \u00a9 2015 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.",
+    "features": [],
+    "query": [
+        "No Results"
+    ],
+    "type": "FeatureCollection"
+}
diff --git a/test/fixtures/mapquest_error b/test/fixtures/mapquest_error
new file mode 100644
index 0000000..0f8adf2
--- /dev/null
+++ b/test/fixtures/mapquest_error
@@ -0,0 +1,16 @@
+{
+  "results": [
+    {
+    "locations": []
+    }
+  ],
+  "info": {
+    "copyright": {
+      "text": "© 2012 MapQuest, Inc.",
+      "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+      "imageAltText": "© 2012 MapQuest, Inc."
+    },
+    "statuscode": 500,
+    "messages": ["Error processing request: ..."]
+  }
+}
diff --git a/test/fixtures/mapquest_invalid_api_key b/test/fixtures/mapquest_invalid_api_key
new file mode 100644
index 0000000..f8df9eb
--- /dev/null
+++ b/test/fixtures/mapquest_invalid_api_key
@@ -0,0 +1,16 @@
+{
+  "results": [
+    {
+    "locations": []
+    }
+  ],
+  "info": {
+    "copyright": {
+      "text": "© 2012 MapQuest, Inc.",
+      "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+      "imageAltText": "© 2012 MapQuest, Inc."
+    },
+    "statuscode": 403,
+    "messages": ["..."]
+  }
+}
diff --git a/test/fixtures/mapquest_invalid_request b/test/fixtures/mapquest_invalid_request
new file mode 100644
index 0000000..00845a6
--- /dev/null
+++ b/test/fixtures/mapquest_invalid_request
@@ -0,0 +1,16 @@
+{
+  "results": [
+    {
+    "locations": []
+    }
+  ],
+  "info": {
+    "copyright": {
+      "text": "© 2012 MapQuest, Inc.",
+      "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+      "imageAltText": "© 2012 MapQuest, Inc."
+    },
+    "statuscode": 400,
+    "messages": ["Illegal argument from request: ..."]
+  }
+}
diff --git a/test/fixtures/mapquest_madison_square_garden b/test/fixtures/mapquest_madison_square_garden
new file mode 100644
index 0000000..86c135b
--- /dev/null
+++ b/test/fixtures/mapquest_madison_square_garden
@@ -0,0 +1,52 @@
+{
+  "results": [
+    {
+      "locations": [
+        {
+          "latLng": {
+            "lng": -73.994637,
+            "lat": 40.720409
+          },
+          "adminArea4": "New York County",
+          "adminArea5Type": "City",
+          "adminArea4Type": "County",
+          "adminArea5": "New York",
+          "street": "46 West 31st Street",
+          "adminArea1": "US",
+          "adminArea3": "NY",
+          "type": "s",
+          "displayLatLng": {
+            "lng": -73.994637,
+            "lat": 40.720409
+          },
+          "linkId": 0,
+          "postalCode": "10001",
+          "sideOfStreet": "N",
+          "dragPoint": false,
+          "adminArea1Type": "Country",
+          "geocodeQuality": "CITY",
+          "geocodeQualityCode": "A5XAX",
+          "mapUrl": "http://www.mapquestapi.com/staticmap/v3/getmap?type=map&size=225,160&pois=purple-1,40.720409,-73.994637,0,0|&center=40.720409,-73.994637&zoom=9&key=Gmjtd|luua2hu2nd,7x=o5-lz8lg&rand=604519389",
+          "adminArea3Type": "State"
+        }
+      ],
+      "providedLocation": {
+        "location": "Madison Square Garden, New York, NY"
+      }
+    }
+  ],
+  "options": {
+    "ignoreLatLngInput": false,
+    "maxResults": -1,
+    "thumbMaps": true
+  },
+  "info": {
+    "copyright": {
+      "text": "© 2012 MapQuest, Inc.",
+      "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+      "imageAltText": "© 2012 MapQuest, Inc."
+    },
+    "statuscode": 0,
+    "messages": []
+  }
+}
diff --git a/test/fixtures/mapquest_no_results b/test/fixtures/mapquest_no_results
new file mode 100644
index 0000000..b392476
--- /dev/null
+++ b/test/fixtures/mapquest_no_results
@@ -0,0 +1,16 @@
+{
+  "results": [
+    {
+    "locations": []
+    }
+  ],
+  "info": {
+    "copyright": {
+      "text": "© 2012 MapQuest, Inc.",
+      "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+      "imageAltText": "© 2012 MapQuest, Inc."
+    },
+    "statuscode": 0,
+    "messages": []
+  }
+}
diff --git a/test/fixtures/maxmind_24_24_24_21 b/test/fixtures/maxmind_24_24_24_21
new file mode 100644
index 0000000..f4cd62e
--- /dev/null
+++ b/test/fixtures/maxmind_24_24_24_21
@@ -0,0 +1 @@
+US
diff --git a/test/fixtures/maxmind_24_24_24_22 b/test/fixtures/maxmind_24_24_24_22
new file mode 100644
index 0000000..b9aee13
--- /dev/null
+++ b/test/fixtures/maxmind_24_24_24_22
@@ -0,0 +1 @@
+US,NY,Jamaica,40.6915,-73.8057
diff --git a/test/fixtures/maxmind_24_24_24_23 b/test/fixtures/maxmind_24_24_24_23
new file mode 100644
index 0000000..29ecfb2
--- /dev/null
+++ b/test/fixtures/maxmind_24_24_24_23
@@ -0,0 +1 @@
+US,NY,Jamaica,,40.6915,-73.8057,501,718,"Road Runner","Road Runner"
diff --git a/test/fixtures/maxmind_24_24_24_24 b/test/fixtures/maxmind_24_24_24_24
new file mode 100644
index 0000000..1493903
--- /dev/null
+++ b/test/fixtures/maxmind_24_24_24_24
@@ -0,0 +1 @@
+US,"United States",NY,"New York",Jamaica,40.6915,-73.8057,501,718,America/New_York,NA,,"Road Runner","Road Runner",rr.com,"AS11351 Road Runner HoldCo LLC",Cable/DSL,residential,779,99,37,76,35
diff --git a/test/fixtures/maxmind_74_200_247_59 b/test/fixtures/maxmind_74_200_247_59
new file mode 100644
index 0000000..9eb1bd0
--- /dev/null
+++ b/test/fixtures/maxmind_74_200_247_59
@@ -0,0 +1 @@
+US,TX,Plano,75093,33.034698,-96.813400,623,972,"Layered Technologies , US","Layered Technologies , US",
diff --git a/test/fixtures/maxmind_geoip2_1_2_3_4 b/test/fixtures/maxmind_geoip2_1_2_3_4
new file mode 100644
index 0000000..b6cb11e
--- /dev/null
+++ b/test/fixtures/maxmind_geoip2_1_2_3_4
@@ -0,0 +1,116 @@
+{
+  "city":  {
+      "confidence":  25,
+      "geoname_id": 54321,
+      "names":  {
+          "de":    "Los Angeles",
+          "en":    "Los Angeles",
+          "es":    "Los Ángeles",
+          "fr":    "Los Angeles",
+          "ja":    "ロサンゼルス市",
+          "pt-BR":  "Los Angeles",
+          "ru":    "Лос-Анджелес",
+          "zh-CN": "洛杉矶"
+      }
+  },
+  "continent":  {
+      "code":       "NA",
+      "geoname_id": 123456,
+      "names":  {
+          "de":    "Nordamerika",
+          "en":    "North America",
+          "es":    "América del Norte",
+          "fr":    "Amérique du Nord",
+          "ja":    "北アメリカ",
+          "pt-BR": "América do Norte",
+          "ru":    "Северная Америка",
+          "zh-CN": "北美洲"
+ 
+      }
+  },
+  "country":  {
+      "confidence":  75,
+      "geoname_id":  6252001,
+      "iso_code":    "US",
+      "names":  {
+          "de":     "USA",
+          "en":     "United States",
+          "es":     "Estados Unidos",
+          "fr":     "États-Unis",
+          "ja":     "アメリカ合衆国",
+          "pt-BR":  "Estados Unidos",
+          "ru":     "США",
+          "zh-CN":  "美国"
+      }
+  },
+  "location":  {
+      "accuracy_radius":   20,
+      "latitude":          37.6293,
+      "longitude":         -122.1163,
+      "metro_code":        807,
+      "time_zone":         "America/Los_Angeles"
+  },
+  "postal": {
+      "code":       "90001",
+      "confidence": 10
+  },
+  "registered_country":  {
+      "geoname_id":  6252001,
+      "iso_code":    "US",
+      "names":  {
+          "de":     "USA",
+          "en":     "United States",
+          "es":     "Estados Unidos",
+          "fr":     "États-Unis",
+          "ja":     "アメリカ合衆国",
+          "pt-BR":  "Estados Unidos",
+          "ru":     "США",
+          "zh-CN":  "美国"
+      }
+  },
+  "represented_country":  {
+      "geoname_id":  6252001,
+      "iso_code":    "US",
+      "names":  {
+          "de":     "USA",
+          "en":     "United States",
+          "es":     "Estados Unidos",
+          "fr":     "États-Unis",
+          "ja":     "アメリカ合衆国",
+          "pt-BR":  "Estados Unidos",
+          "ru":     "США",
+          "zh-CN":  "美国"
+      },
+      "type": "military"
+  },
+  "subdivisions":  [
+      {
+          "confidence":  50,
+          "geoname_id":  5332921,
+          "iso_code":    "CA",
+          "names":  {
+              "de":    "Kalifornien",
+              "en":    "California",
+              "es":    "California",
+              "fr":    "Californie",
+              "ja":    "カリフォルニア",
+              "ru":    "Калифорния",
+              "zh-CN": "加州"
+          }
+      }
+  ],
+  "traits": {
+      "autonomous_system_number":      1239,
+      "autonomous_system_organization": "Linkem IR WiMax Network",
+      "domain":                        "example.com",
+      "is_anonymous_proxy":            true,
+      "is_satellite_provider":         true,
+      "isp":                           "Linkem spa",
+      "ip_address":                    "1.2.3.4",
+      "organization":                  "Linkem IR WiMax Network",
+      "user_type":                     "traveler"
+  },
+  "maxmind": {
+      "queries_remaining":            54321
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/maxmind_geoip2_no_results b/test/fixtures/maxmind_geoip2_no_results
new file mode 100644
index 0000000..e69de29
diff --git a/test/fixtures/maxmind_invalid_key b/test/fixtures/maxmind_invalid_key
new file mode 100644
index 0000000..e216586
--- /dev/null
+++ b/test/fixtures/maxmind_invalid_key
@@ -0,0 +1 @@
+,,,,,,,,,,INVALID_LICENSE_KEY
diff --git a/test/fixtures/maxmind_no_results b/test/fixtures/maxmind_no_results
new file mode 100644
index 0000000..c1d1bc2
--- /dev/null
+++ b/test/fixtures/maxmind_no_results
@@ -0,0 +1 @@
+,,,,,,,,,,IP_NOT_FOUND
\ No newline at end of file
diff --git a/test/fixtures/melissa_street_invalid_key b/test/fixtures/melissa_street_invalid_key
new file mode 100644
index 0000000..c34ef91
--- /dev/null
+++ b/test/fixtures/melissa_street_invalid_key
@@ -0,0 +1,6 @@
+{
+  "Version": "3.0.1.167",
+  "TransmissionReference": "",
+  "TransmissionResults": "GE05",
+  "TotalRecords": "0"
+}
diff --git a/test/fixtures/melissa_street_low_accuracy b/test/fixtures/melissa_street_low_accuracy
new file mode 100644
index 0000000..cd889c2
--- /dev/null
+++ b/test/fixtures/melissa_street_low_accuracy
@@ -0,0 +1,73 @@
+{
+  "Version": "3.0.1.167",
+  "TransmissionReference": "",
+  "TransmissionResults": "",
+  "TotalRecords": "1",
+  "Records": [
+    {
+      "RecordID": "1",
+      "Results": "AC16,AE11,AV13,GS03",
+      "FormattedAddress": "Frank H Ogawa Plaza;Oakland, CA 94612",
+      "Organization": "",
+      "AddressLine1": "Frank H Ogawa Plaza",
+      "AddressLine2": "Oakland, CA 94612",
+      "AddressLine3": "",
+      "AddressLine4": "",
+      "AddressLine5": "",
+      "AddressLine6": "",
+      "AddressLine7": "",
+      "AddressLine8": "",
+      "SubPremises": "",
+      "DoubleDependentLocality": "",
+      "DependentLocality": "",
+      "Locality": "Oakland",
+      "SubAdministrativeArea": "Alameda",
+      "AdministrativeArea": "CA",
+      "PostalCode": "94612",
+      "PostalCodeType": " ",
+      "AddressType": " ",
+      "AddressKey": "94612000000",
+      "SubNationalArea": "",
+      "CountryName": "United States of America",
+      "CountryISO3166_1_Alpha2": "US",
+      "CountryISO3166_1_Alpha3": "USA",
+      "CountryISO3166_1_Numeric": "840",
+      "CountrySubdivisionCode": "US-CA",
+      "Thoroughfare": "Frank H Ogawa Plz",
+      "ThoroughfarePreDirection": "",
+      "ThoroughfareLeadingType": "",
+      "ThoroughfareName": "Frank H Ogawa",
+      "ThoroughfareTrailingType": "Plz",
+      "ThoroughfarePostDirection": "",
+      "DependentThoroughfare": "",
+      "DependentThoroughfarePreDirection": "",
+      "DependentThoroughfareLeadingType": "",
+      "DependentThoroughfareName": "",
+      "DependentThoroughfareTrailingType": "",
+      "DependentThoroughfarePostDirection": "",
+      "Building": "",
+      "PremisesType": "",
+      "PremisesNumber": "",
+      "SubPremisesType": "",
+      "SubPremisesNumber": "",
+      "PostBox": "",
+      "Latitude": "37.806200",
+      "Longitude": "-122.268900",
+      "DeliveryIndicator": "U",
+      "MelissaAddressKey": "",
+      "MelissaAddressKeyBase": "",
+      "PostOfficeLocation": "",
+      "SubPremiseLevel": "",
+      "SubPremiseLevelType": "",
+      "SubPremiseLevelNumber": "",
+      "SubBuilding": "",
+      "SubBuildingType": "",
+      "SubBuildingNumber": "",
+      "UTC": "UTC-08:00",
+      "DST": "Y",
+      "DeliveryPointSuffix": "",
+      "CensusKey": "060014029001027",
+      "Extras": {}
+    }
+  ]
+}
diff --git a/test/fixtures/melissa_street_oakland_city_hall b/test/fixtures/melissa_street_oakland_city_hall
new file mode 100644
index 0000000..fc92cac
--- /dev/null
+++ b/test/fixtures/melissa_street_oakland_city_hall
@@ -0,0 +1,73 @@
+{
+  "Version": "3.0.1.167",
+  "TransmissionReference": "",
+  "TransmissionResults": "",
+  "TotalRecords": "1",
+  "Records": [
+    {
+      "RecordID": "1",
+      "Results": "AC10,AC14,AC16,AV14,GS05",
+      "FormattedAddress": "1 Frank H Ogawa Plz Fl 3;Oakland, CA 94612-1932",
+      "Organization": "",
+      "AddressLine1": "1 Frank H Ogawa Plz Fl 3",
+      "AddressLine2": "Oakland, CA 94612-1932",
+      "AddressLine3": "",
+      "AddressLine4": "",
+      "AddressLine5": "",
+      "AddressLine6": "",
+      "AddressLine7": "",
+      "AddressLine8": "",
+      "SubPremises": "Fl 3",
+      "DoubleDependentLocality": "",
+      "DependentLocality": "",
+      "Locality": "Oakland",
+      "SubAdministrativeArea": "Alameda",
+      "AdministrativeArea": "CA",
+      "PostalCode": "94612-1932",
+      "PostalCodeType": " ",
+      "AddressType": "H",
+      "AddressKey": "94612193299",
+      "SubNationalArea": "",
+      "CountryName": "United States of America",
+      "CountryISO3166_1_Alpha2": "US",
+      "CountryISO3166_1_Alpha3": "USA",
+      "CountryISO3166_1_Numeric": "840",
+      "CountrySubdivisionCode": "US-CA",
+      "Thoroughfare": "Frank H Ogawa Plz",
+      "ThoroughfarePreDirection": "",
+      "ThoroughfareLeadingType": "",
+      "ThoroughfareName": "Frank H Ogawa",
+      "ThoroughfareTrailingType": "Plz",
+      "ThoroughfarePostDirection": "",
+      "DependentThoroughfare": "",
+      "DependentThoroughfarePreDirection": "",
+      "DependentThoroughfareLeadingType": "",
+      "DependentThoroughfareName": "",
+      "DependentThoroughfareTrailingType": "",
+      "DependentThoroughfarePostDirection": "",
+      "Building": "",
+      "PremisesType": "",
+      "PremisesNumber": "1",
+      "SubPremisesType": "Fl",
+      "SubPremisesNumber": "3",
+      "PostBox": "",
+      "Latitude": "37.805402",
+      "Longitude": "-122.272797",
+      "DeliveryIndicator": "B",
+      "MelissaAddressKey": "9772863955",
+      "MelissaAddressKeyBase": "",
+      "PostOfficeLocation": "",
+      "SubPremiseLevel": "",
+      "SubPremiseLevelType": "",
+      "SubPremiseLevelNumber": "",
+      "SubBuilding": "",
+      "SubBuildingType": "",
+      "SubBuildingNumber": "",
+      "UTC": "UTC-08:00",
+      "DST": "Y",
+      "DeliveryPointSuffix": "",
+      "CensusKey": "060014028002040",
+      "Extras": {}
+    }
+  ]
+}
diff --git a/test/fixtures/nationaal_georegister_nl b/test/fixtures/nationaal_georegister_nl
new file mode 100644
index 0000000..a41c0ca
--- /dev/null
+++ b/test/fixtures/nationaal_georegister_nl
@@ -0,0 +1,441 @@
+{
+  "response": {
+    "numFound": 10242,
+    "start": 0,
+    "maxScore": 61.028397,
+    "docs": [
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "147",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000218908",
+        "weergavenaam": "Nieuwezijds Voorburgwal 147, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 147, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 147, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-563f90756e46d1c554b4ab00ac61b932",
+        "gekoppeld_perceel": [
+          "ASD04-F-2749"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010000758545-0363200000218908",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89089949 52.37316397)",
+        "centroide_ll": "POINT(4.89089949 52.37316397)",
+        "nummeraanduiding_id": "0363200000218908",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010000758545",
+        "huisnummer": 147,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121202 487370)",
+        "geometrie_rd": "POINT(121202 487370)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627309871432990700,
+        "typesortering": 4,
+        "sortering": 147,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "suggest": [
+          "Nieuwezijds Voorburgwal, 1012 RJ Amsterdam",
+          "Nieuwezijds Voorburgwal, 1012RJ Amsterdam"
+        ],
+        "woonplaatscode": "3594",
+        "type": "postcode",
+        "woonplaatsnaam": "Amsterdam",
+        "openbareruimtetype": "Weg",
+        "gemeentecode": "0363",
+        "weergavenaam": "Nieuwezijds Voorburgwal, 1012RJ Amsterdam",
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "pcd-07ad1c9a6086b4ffd4b885b4cc12f513",
+        "gemeentenaam": "Amsterdam",
+        "identificatie": "0363300000004690_1012RJ",
+        "openbareruimte_id": "0363300000004690",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89133064 52.3739109)",
+        "centroide_ll": "POINT(4.89133064 52.3739109)",
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121231.93 487452.904)",
+        "geometrie_rd": "POINT(121231.93 487452.904)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627316920607834000,
+        "typesortering": 3.5,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "125",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000218874",
+        "weergavenaam": "Nieuwezijds Voorburgwal 125, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 125, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 125, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-00c3407685fcd67989bb2915acdee929",
+        "gekoppeld_perceel": [
+          "ASD04-F-2732"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010000758517-0363200000218874",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89153483 52.37412832)",
+        "centroide_ll": "POINT(4.89153483 52.37412832)",
+        "nummeraanduiding_id": "0363200000218874",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010000758517",
+        "huisnummer": 125,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121246 487477)",
+        "geometrie_rd": "POINT(121246 487477)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627309871418310700,
+        "typesortering": 4,
+        "sortering": 125,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "127",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012076682",
+        "weergavenaam": "Nieuwezijds Voorburgwal 127, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 127, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 127, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-bb2a9b0c4090656cd8a31e3cdfa26701",
+        "gekoppeld_perceel": [
+          "ASD04-F-2731"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010012076291-0363200012076682",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89147613 52.37412482)",
+        "centroide_ll": "POINT(4.89147613 52.37412482)",
+        "nummeraanduiding_id": "0363200012076682",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010012076291",
+        "huisnummer": 127,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121242 487476.638)",
+        "geometrie_rd": "POINT(121242 487476.638)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627310053758337000,
+        "typesortering": 4,
+        "sortering": 127,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "129",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012076683",
+        "weergavenaam": "Nieuwezijds Voorburgwal 129, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 129, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 129, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-e3339f855fd23024786c719e404fe9e8",
+        "gekoppeld_perceel": [
+          "ASD04-F-2729"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010012076292-0363200012076683",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89141948 52.37411589)",
+        "centroide_ll": "POINT(4.89141948 52.37411589)",
+        "nummeraanduiding_id": "0363200012076683",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010012076292",
+        "huisnummer": 129,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121238.135 487475.671)",
+        "geometrie_rd": "POINT(121238.135 487475.671)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627310053759385600,
+        "typesortering": 4,
+        "sortering": 129,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "133",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000218889",
+        "weergavenaam": "Nieuwezijds Voorburgwal 133, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 133, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 133, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-0c4f985c4b63168eb4327f76774deef1",
+        "gekoppeld_perceel": [
+          "ASD04-F-7847"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010000758534-0363200000218889",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89128581 52.37407335)",
+        "centroide_ll": "POINT(4.89128581 52.37407335)",
+        "nummeraanduiding_id": "0363200000218889",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010000758534",
+        "huisnummer": 133,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121229 487471)",
+        "geometrie_rd": "POINT(121229 487471)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627309871428796400,
+        "typesortering": 4,
+        "sortering": 133,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "135",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000218892",
+        "weergavenaam": "Nieuwezijds Voorburgwal 135, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 135, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 135, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-741c0d7a8cab9ea398e8e5de0fb6e841",
+        "gekoppeld_perceel": [
+          "ASD04-F-7847"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010000758537-0363200000218892",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89122747 52.37403716)",
+        "centroide_ll": "POINT(4.89122747 52.37403716)",
+        "nummeraanduiding_id": "0363200000218892",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010000758537",
+        "huisnummer": 135,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121225 487467)",
+        "geometrie_rd": "POINT(121225 487467)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627309871429845000,
+        "typesortering": 4,
+        "sortering": 135,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "137",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000218895",
+        "weergavenaam": "Nieuwezijds Voorburgwal 137, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 137, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 137, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-49b25fb1d14a09d548beb984452010a8",
+        "gekoppeld_perceel": [
+          "ASD04-F-7847"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010000758540-0363200000218895",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89116944 52.373974)",
+        "centroide_ll": "POINT(4.89116944 52.373974)",
+        "nummeraanduiding_id": "0363200000218895",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010000758540",
+        "huisnummer": 137,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121221 487460)",
+        "geometrie_rd": "POINT(121221 487460)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627309871430893600,
+        "typesortering": 4,
+        "sortering": 137,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "141",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000218902",
+        "weergavenaam": "Nieuwezijds Voorburgwal 141, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 141, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 141, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-2e8a1e522b5cacd766fbe23b58da4f8c",
+        "gekoppeld_perceel": [
+          "ASD04-F-5658"
+        ],
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363010000758542-0363200000218902",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89108546 52.37390916)",
+        "centroide_ll": "POINT(4.89108546 52.37390916)",
+        "nummeraanduiding_id": "0363200000218902",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363010000758542",
+        "huisnummer": 141,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121215.232 487452.825)",
+        "geometrie_rd": "POINT(121215.232 487452.825)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627309871430893600,
+        "typesortering": 4,
+        "sortering": 141,
+        "shard": "bag"
+      },
+      {
+        "bron": "BAG",
+        "woonplaatscode": "3594",
+        "type": "adres",
+        "woonplaatsnaam": "Amsterdam",
+        "wijkcode": "WK036301",
+        "huis_nlt": "143",
+        "openbareruimtetype": "Weg",
+        "buurtnaam": "Nieuwe Kerk e.o.",
+        "gemeentecode": "0363",
+        "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012093196",
+        "weergavenaam": "Nieuwezijds Voorburgwal 143, 1012RJ Amsterdam",
+        "suggest": [
+          "Nieuwezijds Voorburgwal 143, 1012RJ Amsterdam",
+          "Nieuwezijds Voorburgwal 143, 1012 RJ Amsterdam"
+        ],
+        "straatnaam_verkort": "Nieuwezijds Voorburgwal",
+        "id": "adr-5b6658e7817f0c7fcf29b7874b4e396c",
+        "gemeentenaam": "Amsterdam",
+        "buurtcode": "BU03630104",
+        "wijknaam": "Burgwallen-Nieuwe Zijde",
+        "identificatie": "0363200012093196-0363200012093196",
+        "openbareruimte_id": "0363300000004690",
+        "waterschapsnaam": "HH Amstel, Gooi en Vecht",
+        "provinciecode": "PV27",
+        "postcode": "1012RJ",
+        "provincienaam": "Noord-Holland",
+        "geometrie_ll": "POINT(4.89187769 52.37367138)",
+        "centroide_ll": "POINT(4.89187769 52.37367138)",
+        "nummeraanduiding_id": "0363200012093196",
+        "waterschapscode": "31",
+        "adresseerbaarobject_id": "0363200012093196",
+        "huisnummer": 143,
+        "provincieafkorting": "NH",
+        "centroide_rd": "POINT(121269 487426)",
+        "geometrie_rd": "POINT(121269 487426)",
+        "straatnaam": "Nieuwezijds Voorburgwal",
+        "_version_": 1627310104285020200,
+        "typesortering": 4,
+        "sortering": 143,
+        "shard": "bag"
+      }
+    ]
+  }
+}
diff --git a/test/fixtures/nominatim_cologne_cathedral_cologne_germany b/test/fixtures/nominatim_cologne_cathedral_cologne_germany
new file mode 100644
index 0000000..5c7b774
--- /dev/null
+++ b/test/fixtures/nominatim_cologne_cathedral_cologne_germany
@@ -0,0 +1,36 @@
+[
+  {
+    "place_id": "61478831",
+    "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
+    "osm_type": "way",
+    "osm_id": "4532022",
+    "boundingbox": [
+      "50.9409217",
+      "50.9417618",
+      "6.9570708",
+      "6.9591382"
+    ],
+    "lat": "50.94134445",
+    "lon": "6.95812085888689",
+    "display_name": "Cologne Cathedral, 4, Domkloster, Kunibertsviertel, Altstadt-Nord, Innenstadt, Cologne, Cologne Government Region, North Rhine-Westphalia, 50667, Germany",
+    "class": "amenity",
+    "type": "place_of_worship",
+    "importance": 0.70253720393793,
+    "icon": "http://nominatim.openstreetmap.org/images/mapicons/place_of_worship_unknown3.p.20.png",
+    "address": {
+      "place_of_worship": "Cologne Cathedral",
+      "house_number": "4",
+      "pedestrian": "Domkloster",
+      "neighbourhood": "Kunibertsviertel",
+      "suburb": "Altstadt-Nord",
+      "city_district": "Innenstadt",
+      "city": "Cologne",
+      "county": "Cologne",
+      "state_district": "Cologne Government Region",
+      "state": "North Rhine-Westphalia",
+      "postcode": "50667",
+      "country": "Germany",
+      "country_code": "de"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/nominatim_madison_square_garden b/test/fixtures/nominatim_madison_square_garden
new file mode 100644
index 0000000..a1d709e
--- /dev/null
+++ b/test/fixtures/nominatim_madison_square_garden
@@ -0,0 +1,150 @@
+[
+
+    {
+        "place_id": "30632629",
+        "licence": "Data Copyright OpenStreetMap Contributors, Some Rights Reserved. CC-BY-SA 2.0.",
+        "osm_type": "way",
+        "osm_id": "24801588",
+        "boundingbox": [
+            "40.749828338623",
+            "40.7511596679688",
+            "-73.9943389892578",
+            "-73.9926528930664"
+        ],
+        "polygonpoints": [
+            [
+                "-73.9943346",
+                "40.7503638"
+            ],
+            [
+                "-73.9942745",
+                "40.7504158"
+            ],
+            [
+                "-73.9942593",
+                "40.750629"
+            ],
+            [
+                "-73.9941343",
+                "40.7508432"
+            ],
+            [
+                "-73.9939794",
+                "40.7509703"
+            ],
+            [
+                "-73.9938042",
+                "40.7510532"
+            ],
+            [
+                "-73.9938025",
+                "40.7511311"
+            ],
+            [
+                "-73.9936051",
+                "40.7511571"
+            ],
+            [
+                "-73.9935673",
+                "40.751105"
+            ],
+            [
+                "-73.9934095",
+                "40.7511089"
+            ],
+            [
+                "-73.9931235",
+                "40.7510548"
+            ],
+            [
+                "-73.9928863",
+                "40.7509311"
+            ],
+            [
+                "-73.9928068",
+                "40.750949"
+            ],
+            [
+                "-73.992721",
+                "40.7508515"
+            ],
+            [
+                "-73.9927444",
+                "40.7507889"
+            ],
+            [
+                "-73.9926693",
+                "40.7506457"
+            ],
+            [
+                "-73.9926597",
+                "40.7503657"
+            ],
+            [
+                "-73.9928305",
+                "40.7500953"
+            ],
+            [
+                "-73.9929757",
+                "40.7499911"
+            ],
+            [
+                "-73.9931281",
+                "40.7499238"
+            ],
+            [
+                "-73.993133",
+                "40.7498631"
+            ],
+            [
+                "-73.9932961",
+                "40.7498306"
+            ],
+            [
+                "-73.9933664",
+                "40.7498742"
+            ],
+            [
+                "-73.993471",
+                "40.7498701"
+            ],
+            [
+                "-73.9938023",
+                "40.7499263"
+            ],
+            [
+                "-73.9940703",
+                "40.7500756"
+            ],
+            [
+                "-73.9941876",
+                "40.7502038"
+            ],
+            [
+                "-73.9942831",
+                "40.7502142"
+            ],
+            [
+                "-73.9943346",
+                "40.7503638"
+            ]
+        ],
+        "lat": "40.7504928941818",
+        "lon": "-73.993466492276",
+        "display_name": "Madison Square Garden, West 31st Street, Long Island City, New York City, New York, 10001, United States of America",
+        "class": "leisure",
+        "type": "stadium",
+        "address": {
+            "stadium": "Madison Square Garden",
+            "road": "West 31st Street",
+            "suburb": "Long Island City",
+            "city": "New York City",
+            "county": "New York",
+            "state": "New York",
+            "postcode": "10001",
+            "country": "United States of America",
+            "country_code": "us"
+        }
+    }
+
+]
\ No newline at end of file
diff --git a/test/fixtures/nominatim_no_results b/test/fixtures/nominatim_no_results
new file mode 100644
index 0000000..1e3ec72
--- /dev/null
+++ b/test/fixtures/nominatim_no_results
@@ -0,0 +1 @@
+[ ]
diff --git a/test/fixtures/nominatim_over_limit b/test/fixtures/nominatim_over_limit
new file mode 100644
index 0000000..9b9f64d
--- /dev/null
+++ b/test/fixtures/nominatim_over_limit
@@ -0,0 +1 @@
+<html>\n<head>\n<title>Bandwidth limit exceeded</title>\n</head>\n<body>\n<h1>Bandwidth limit exceeded</h1>\n\n<p>You have been temporarily blocked because you have been overusing OSM's geocoding service or because you have not provided sufficient identification of your application. This block will be automatically lifted after a while. Please take the time and adapt your scripts to reduce the number of requests and make sure that you send a valid UserAgent or Referer.</p>\n\n<p>For more information, consult the <a href=\"http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy\">usage policy</a> for the OSM Nominatim server.\n</body>\n</head>\n
diff --git a/test/fixtures/opencagedata_invalid_api_key b/test/fixtures/opencagedata_invalid_api_key
new file mode 100644
index 0000000..08bd98e
--- /dev/null
+++ b/test/fixtures/opencagedata_invalid_api_key
@@ -0,0 +1,25 @@
+{
+    "licenses": [
+        {
+            "name": "CC-BY-SA",
+            "url": "http://creativecommons.org/licenses/by-sa/3.0/"
+        },
+        {
+            "name": "ODbL",
+            "url": "http://opendatacommons.org/licenses/odbl/summary/"
+        }
+    ],
+    "results": [ ],
+    "status": {
+        "code": 403,
+        "message": "invalid API key"
+    },
+    "thanks": "For using an OpenCage Data API",
+    "timestamp": {
+        "created_http": "Thu, 07 Aug 2014 14:26:28 GMT",
+        "created_unix": 1407421588
+    },
+    "total_results": 0,
+    "we_are_hiring": "http://lokku.com/#jobs"
+
+}
\ No newline at end of file
diff --git a/test/fixtures/opencagedata_invalid_request b/test/fixtures/opencagedata_invalid_request
new file mode 100644
index 0000000..1f6b488
--- /dev/null
+++ b/test/fixtures/opencagedata_invalid_request
@@ -0,0 +1,26 @@
+{
+
+    "licenses": [
+        {
+            "name": "CC-BY-SA",
+            "url": "http://creativecommons.org/licenses/by-sa/3.0/"
+        },
+        {
+            "name": "ODbL",
+            "url": "http://opendatacommons.org/licenses/odbl/summary/"
+        }
+    ],
+    "results": [ ],
+    "status": {
+        "code": 400,
+        "message": "Attribute (format) does not pass the type constraint because: not a valid format"
+    },
+    "thanks": "For using an OpenCage Data API",
+    "timestamp": {
+        "created_http": "Thu, 07 Aug 2014 14:27:29 GMT",
+        "created_unix": 1407421649
+    },
+    "total_results": 0,
+    "we_are_hiring": "http://lokku.com/#jobs"
+
+}
\ No newline at end of file
diff --git a/test/fixtures/opencagedata_madison_square_garden b/test/fixtures/opencagedata_madison_square_garden
new file mode 100644
index 0000000..b5f1da0
--- /dev/null
+++ b/test/fixtures/opencagedata_madison_square_garden
@@ -0,0 +1,74 @@
+{
+   "licenses" : [
+      {
+         "name" : "CC-BY-SA",
+         "url" : "http://creativecommons.org/licenses/by-sa/3.0/"
+      },
+      {
+         "name" : "ODbL",
+         "url" : "http://opendatacommons.org/licenses/odbl/summary/"
+      }
+   ],
+   "rate" : {
+      "limit" : 2500,
+      "remaining" : 2488,
+      "reset" : 1407369600
+   },
+   "results" : [
+      {
+         "annotations" : {
+            "OSM" : {
+               "url" : "http://www.openstreetmap.org/?mlat=40.75052&mlon=-73.99355#map=17/40.75052/-73.99355"
+            },
+            "timezone" : {
+               "name" : "America/New_York",
+               "now_in_dst" : 1,
+               "offset_sec" : -14400,
+               "offset_string" : -400,
+               "short_name" : "EDT"
+            }
+         },
+         "bounds" : {
+            "northeast" : {
+               "lat" : 40.751161,
+               "lng" : -73.9925922
+            },
+            "southwest" : {
+               "lat" : 40.7498531,
+               "lng" : -73.9944444
+            }
+         },
+         "components" : {
+            "country" : "United States of America",
+            "country_code" : "US",
+            "county" : "New York County",
+            "house_number" : 46,
+            "neighbourhood" : "Koreatown",
+            "postcode" : 10011,
+            "road" : "West 31st Street",
+            "city": "New York City",
+            "stadium" : "Madison Square Garden",
+            "state" : "New York",
+            "state_code" : "NY",
+            "state_district" : "New York City"
+         },
+         "confidence" : 10,
+         "formatted" : "46, West 31st Street, Koreatown, New York County, 10011, New York City, New York, United States of America, Madison Square Garden",
+         "geometry" : {
+            "lat" : 40.7505247,
+            "lng" : -73.9935500942432
+         }
+      }
+   ],
+   "status" : {
+      "code" : 200,
+      "message" : "OK"
+   },
+   "thanks" : "For using an OpenCage Data API",
+   "timestamp" : {
+      "created_http" : "Wed, 06 Aug 2014 12:53:59 GMT",
+      "created_unix" : 1407329639
+   },
+   "total_results" : 1,
+   "we_are_hiring" : "http://lokku.com/#jobs"
+}
diff --git a/test/fixtures/opencagedata_no_results b/test/fixtures/opencagedata_no_results
new file mode 100644
index 0000000..16fdf7c
--- /dev/null
+++ b/test/fixtures/opencagedata_no_results
@@ -0,0 +1,29 @@
+{
+   "licenses" : [
+      {
+         "name" : "CC-BY-SA",
+         "url" : "http://creativecommons.org/licenses/by-sa/3.0/"
+      },
+      {
+         "name" : "ODbL",
+         "url" : "http://opendatacommons.org/licenses/odbl/summary/"
+      }
+   ],
+   "rate" : {
+      "limit" : 2500,
+      "remaining" : 2487,
+      "reset" : 1407369600
+   },
+   "results" : [],
+   "status" : {
+      "code" : 200,
+      "message" : "OK"
+   },
+   "thanks" : "For using an OpenCage Data API",
+   "timestamp" : {
+      "created_http" : "Wed, 06 Aug 2014 12:56:03 GMT",
+      "created_unix" : 1407329763
+   },
+   "total_results" : 0,
+   "we_are_hiring" : "http://lokku.com/#jobs"
+}
diff --git a/test/fixtures/opencagedata_over_limit b/test/fixtures/opencagedata_over_limit
new file mode 100644
index 0000000..d362a22
--- /dev/null
+++ b/test/fixtures/opencagedata_over_limit
@@ -0,0 +1,31 @@
+{
+
+    "licenses": [
+        {
+            "name": "CC-BY-SA",
+            "url": "http://creativecommons.org/licenses/by-sa/3.0/"
+        },
+        {
+            "name": "ODbL",
+            "url": "http://opendatacommons.org/licenses/odbl/summary/"
+        }
+    ],
+    "rate": {
+        "limit": 1,
+        "remaining": 0,
+        "reset": null
+    },
+    "results": [ ],
+    "status": {
+        "code": 402,
+        "message": "quota exceeded"
+    },
+    "thanks": "For using an OpenCage Data API",
+    "timestamp": {
+        "created_http": "Thu, 07 Aug 2014 13:59:19 GMT",
+        "created_unix": 1407419959
+    },
+    "total_results": 0,
+    "we_are_hiring": "http://lokku.com/#jobs"
+
+}
\ No newline at end of file
diff --git a/test/fixtures/osmnames_invalid_request b/test/fixtures/osmnames_invalid_request
new file mode 100644
index 0000000..520d58a
--- /dev/null
+++ b/test/fixtures/osmnames_invalid_request
@@ -0,0 +1,7 @@
+{
+  "count": 20,
+    "startIndex": 0,
+    "message": "Invalid attribute value.",
+    "totalResults": 0,
+    "results": []
+}
diff --git a/test/fixtures/osmnames_madison_square_garden b/test/fixtures/osmnames_madison_square_garden
new file mode 100644
index 0000000..d6b7be0
--- /dev/null
+++ b/test/fixtures/osmnames_madison_square_garden
@@ -0,0 +1,40 @@
+{
+  "count": 20,
+  "nextIndex": 20,
+  "startIndex": 0,
+  "totalResults": 8000,
+  "results": [
+    {
+      "wikipedia": "en:New York City",
+      "rank": 628616.9375,
+      "county": "",
+      "street": "",
+      "wikidata": "Q60",
+      "country_code": "us",
+      "osm_id": "175905",
+      "housenumbers": "",
+      "id": 64,
+      "city": "New York City",
+      "display_name": "New York City, New York, United States of America",
+      "lon": -73.878418,
+      "state": "New York",
+      "boundingbox": [
+        -74.259087,
+        40.477398,
+        -73.70018,
+        40.91618
+      ],
+      "type": "city",
+      "importance": 0.782928,
+      "lat": 40.693073,
+      "class": "place",
+      "name": "New York City",
+      "country": "United States of America",
+      "name_suffix": "New York, US",
+      "osm_type": "relation",
+      "place_rank": 16,
+      "alternative_names": "New York,Nova York,Nueva York,NYC"
+    }
+
+  ]
+}
diff --git a/test/fixtures/osmnames_no_results b/test/fixtures/osmnames_no_results
new file mode 100644
index 0000000..df5cfe5
--- /dev/null
+++ b/test/fixtures/osmnames_no_results
@@ -0,0 +1,6 @@
+{
+  "count": 20,
+  "startIndex": 0,
+  "totalResults": 0,
+  "results": []
+}
diff --git a/test/fixtures/pelias_madison_square_garden b/test/fixtures/pelias_madison_square_garden
new file mode 100644
index 0000000..261d0fb
--- /dev/null
+++ b/test/fixtures/pelias_madison_square_garden
@@ -0,0 +1,326 @@
+{
+  "bbox": [
+    -85.693709,
+    39.942189,
+    -73.9897,
+    40.75066
+  ],
+  "features": [
+    {
+      "geometry": {
+        "coordinates": [
+          -73.99347,
+          40.75066
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "Madison Square Garden Center, Manhattan, NY",
+        "confidence": 0.896,
+        "neighbourhood": "Garment District",
+        "locality": "New York",
+        "localadmin": "Manhattan",
+        "county": "New York County",
+        "region_a": "NY",
+        "region": "New York",
+        "country": "United States",
+        "country_a": "USA",
+        "name": "Madison Square Garden Center",
+        "source": "gn",
+        "layer": "venue",
+        "gid": "gn:venue:5125640",
+        "id": "5125640"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -73.993392,
+          40.750497
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "Madison Square Garden, Manhattan, NY",
+        "confidence": 0.896,
+        "neighbourhood": "Garment District",
+        "locality": "New York",
+        "localadmin": "Manhattan",
+        "county": "New York County",
+        "region_a": "NY",
+        "region": "New York",
+        "country": "United States",
+        "country_a": "USA",
+        "street": "Pennsylvania Plaza",
+        "housenumber": "46",
+        "name": "Madison Square Garden",
+        "source": "osm",
+        "layer": "venue",
+        "gid": "osm:venue:138141251",
+        "id": "138141251"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -73.9897,
+          40.7478
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "Hampton Inn Madison Square Garden, Manhattan, NY",
+        "confidence": 0.885,
+        "neighbourhood": "Koreatown",
+        "locality": "New York",
+        "localadmin": "Manhattan",
+        "county": "New York County",
+        "region_a": "NY",
+        "region": "New York",
+        "country": "United States",
+        "country_a": "USA",
+        "name": "Hampton Inn Madison Square Garden",
+        "source": "gn",
+        "layer": "venue",
+        "gid": "gn:venue:6466291",
+        "id": "6466291"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -73.99541,
+          40.74882
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "Holiday Inn Express NYC Madison Square Garden, Manhattan, NY",
+        "confidence": 0.673,
+        "neighbourhood": "Garment District",
+        "locality": "New York",
+        "localadmin": "Manhattan",
+        "county": "New York County",
+        "region_a": "NY",
+        "region": "New York",
+        "country": "United States",
+        "country_a": "USA",
+        "name": "Holiday Inn Express NYC Madison Square Garden",
+        "source": "gn",
+        "layer": "venue",
+        "gid": "gn:venue:7645793",
+        "id": "7645793"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -73.99,
+          40.748
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "Hampton Inn Madison Square Garden Area Hotel, Manhattan, NY",
+        "confidence": 0.673,
+        "neighbourhood": "Garment District",
+        "locality": "New York",
+        "localadmin": "Manhattan",
+        "county": "New York County",
+        "region_a": "NY",
+        "region": "New York",
+        "country": "United States",
+        "country_a": "USA",
+        "name": "Hampton Inn Madison Square Garden Area Hotel",
+        "source": "gn",
+        "layer": "venue",
+        "gid": "gn:venue:6499758",
+        "id": "6499758"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -75.181686,
+          39.942425
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "2315 Madison Square, Philadelphia, PA",
+        "confidence": 0.5,
+        "neighbourhood": "Schuylkill",
+        "locality": "Philadelphia",
+        "localadmin": "Philadelphia",
+        "county": "Philadelphia County",
+        "region_a": "PA",
+        "region": "Pennsylvania",
+        "country": "United States",
+        "country_a": "USA",
+        "postalcode": "19146",
+        "street": "Madison Square",
+        "housenumber": "2315",
+        "name": "2315 Madison Square",
+        "source": "oa",
+        "layer": "address",
+        "gid": "oa:address:0eef7b448f064f90869b1b4610fb2ccd",
+        "id": "0eef7b448f064f90869b1b4610fb2ccd"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -75.183109,
+          39.942643
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "2419 Madison Square, Philadelphia, PA",
+        "confidence": 0.5,
+        "neighbourhood": "Devil's Pocket",
+        "locality": "Philadelphia",
+        "localadmin": "Philadelphia",
+        "county": "Philadelphia County",
+        "region_a": "PA",
+        "region": "Pennsylvania",
+        "country": "United States",
+        "country_a": "USA",
+        "postalcode": "19146",
+        "street": "Madison Square",
+        "housenumber": "2419",
+        "name": "2419 Madison Square",
+        "source": "oa",
+        "layer": "address",
+        "gid": "oa:address:e7c287fb14f0459f8a1b4d938f57feb9",
+        "id": "e7c287fb14f0459f8a1b4d938f57feb9"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -75.181266,
+          39.942212
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "2304 Madison Square, Philadelphia, PA",
+        "confidence": 0.5,
+        "neighbourhood": "Schuylkill",
+        "locality": "Philadelphia",
+        "localadmin": "Philadelphia",
+        "county": "Philadelphia County",
+        "region_a": "PA",
+        "region": "Pennsylvania",
+        "country": "United States",
+        "country_a": "USA",
+        "postalcode": "19146",
+        "street": "Madison Square",
+        "housenumber": "2304",
+        "name": "2304 Madison Square",
+        "source": "oa",
+        "layer": "address",
+        "gid": "oa:address:fd605b0d1b3a422f9a9b8df9f136a80b",
+        "id": "fd605b0d1b3a422f9a9b8df9f136a80b"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -85.693709,
+          40.131756
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "2200 Madison Square, Anderson, IN",
+        "confidence": 0.5,
+        "neighbourhood": "North Anderson",
+        "locality": "Anderson",
+        "localadmin": "Anderson",
+        "county": "Madison County",
+        "region_a": "IN",
+        "region": "Indiana",
+        "country": "United States",
+        "country_a": "USA",
+        "postalcode": "46011",
+        "street": "Madison Square",
+        "housenumber": "2200",
+        "name": "2200 Madison Square",
+        "source": "oa",
+        "layer": "address",
+        "gid": "oa:address:acd7f29cc4bc44f69c719cf5cdad39e8",
+        "id": "acd7f29cc4bc44f69c719cf5cdad39e8"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "coordinates": [
+          -75.181085,
+          39.942189
+        ],
+        "type": "Point"
+      },
+      "properties": {
+        "label": "2300 Madison Square, Philadelphia, PA",
+        "confidence": 0.5,
+        "neighbourhood": "Schuylkill",
+        "locality": "Philadelphia",
+        "localadmin": "Philadelphia",
+        "county": "Philadelphia County",
+        "region_a": "PA",
+        "region": "Pennsylvania",
+        "country": "United States",
+        "country_a": "USA",
+        "postalcode": "19146",
+        "street": "Madison Square",
+        "housenumber": "2300",
+        "name": "2300 Madison Square",
+        "source": "oa",
+        "layer": "address",
+        "gid": "oa:address:c1d20ac81ac54f35af822359dc70171e",
+        "id": "c1d20ac81ac54f35af822359dc70171e"
+      },
+      "type": "Feature"
+    }
+  ],
+  "type": "FeatureCollection",
+  "geocoding": {
+    "timestamp": 1457996697682,
+    "engine": {
+      "version": "1.0",
+      "author": "Mapzen",
+      "name": "Pelias"
+    },
+    "query": {
+      "querySize": 20,
+      "private": false,
+      "size": 10,
+      "parsed_text": {
+        "admin_parts": "New York, NY 10001, United States\"",
+        "regions": [
+          "Garden",
+          "New York",
+          "10001",
+          "United States\""
+        ],
+        "state": "NY",
+        "street": "\"Madison Square",
+        "name": "\"Madison Square Garden"
+      },
+      "text": "\"Madison Square Garden, New York, NY 10001, United States\""
+    },
+    "attribution": "https:\/\/search.mapzen.com\/v1\/attribution",
+    "version": "0.1"
+  }
+}
diff --git a/test/fixtures/pelias_no_results b/test/fixtures/pelias_no_results
new file mode 100644
index 0000000..7d3c12e
--- /dev/null
+++ b/test/fixtures/pelias_no_results
@@ -0,0 +1,38 @@
+{
+  "bbox": [
+    -85.693709,
+    39.942189,
+    -73.9897,
+    40.75066
+  ],
+  "features": [],
+  "type": "FeatureCollection",
+  "geocoding": {
+    "timestamp": 1457996697682,
+    "engine": {
+      "version": "1.0",
+      "author": "Mapzen",
+      "name": "Pelias"
+    },
+    "query": {
+      "querySize": 20,
+      "private": false,
+      "size": 0,
+      "parsed_text": {
+        "admin_parts": "New York, NY 10001, United States\"",
+        "regions": [
+          "Garden",
+          "New York",
+          "10001",
+          "United States\""
+        ],
+        "state": "NY",
+        "street": "\"Madison Square",
+        "name": "\"Madison Square Garden"
+      },
+      "text": "\"Madison Square Garden, New York, NY 10001, United States\""
+    },
+    "attribution": "https:\/\/search.mapzen.com\/v1\/attribution",
+    "version": "0.1"
+  }
+}
diff --git a/test/fixtures/photon_invalid_request b/test/fixtures/photon_invalid_request
new file mode 100644
index 0000000..b81055f
--- /dev/null
+++ b/test/fixtures/photon_invalid_request
@@ -0,0 +1,3 @@
+{
+  "message":"some error happened and the response code is always 400"
+}
diff --git a/test/fixtures/photon_madison_square_garden b/test/fixtures/photon_madison_square_garden
new file mode 100644
index 0000000..d099a41
--- /dev/null
+++ b/test/fixtures/photon_madison_square_garden
@@ -0,0 +1,34 @@
+{
+  "features":[
+    {
+      "geometry":{
+        "coordinates":[
+          -73.99355027800776,
+          40.7505247
+        ],
+        "type":"Point"
+      },
+      "type":"Feature",
+      "properties":{
+        "osm_id":138141251,
+        "osm_type":"W",
+        "extent":[
+          -73.9944446,
+          40.751161,
+          -73.9925924,
+          40.7498531
+        ],
+        "country":"United States of America",
+        "osm_key":"leisure",
+        "housenumber":"4",
+        "city":"New York",
+        "street":"Pennsylvania Plaza",
+        "osm_value":"stadium",
+        "postcode":"10001",
+        "name":"Madison Square Garden",
+        "state":"New York"
+      }
+    }
+  ],
+  "type":"FeatureCollection"
+}
diff --git a/test/fixtures/photon_no_results b/test/fixtures/photon_no_results
new file mode 100644
index 0000000..44ff530
--- /dev/null
+++ b/test/fixtures/photon_no_results
@@ -0,0 +1,4 @@
+{
+  "type":"FeatureCollection",
+  "features":[]
+}
diff --git a/test/fixtures/photon_reverse b/test/fixtures/photon_reverse
new file mode 100644
index 0000000..1ca04d6
--- /dev/null
+++ b/test/fixtures/photon_reverse
@@ -0,0 +1,27 @@
+{
+  "features":[
+    {
+      "geometry":{
+        "coordinates":[
+          -73.9935078,
+          40.750499
+        ],
+        "type":"Point"
+      },
+      "type":"Feature",
+      "properties":{
+        "osm_id":6985936386,
+        "osm_type":"N",
+        "country":"United States of America",
+        "osm_key":"tourism",
+        "housenumber":"4",
+        "city":"New York",
+        "street":"Pennsylvania Plaza",
+        "osm_value":"attraction",
+        "postcode":"10121",
+        "state":"New York"
+      }
+    }
+  ],
+  "type":"FeatureCollection"
+}
diff --git a/test/fixtures/pickpoint_invalid_api_key b/test/fixtures/pickpoint_invalid_api_key
new file mode 100644
index 0000000..5c6f264
--- /dev/null
+++ b/test/fixtures/pickpoint_invalid_api_key
@@ -0,0 +1 @@
+{"message":"Unauthorized"}
\ No newline at end of file
diff --git a/test/fixtures/pickpoint_madison_square_garden b/test/fixtures/pickpoint_madison_square_garden
new file mode 100644
index 0000000..a1d709e
--- /dev/null
+++ b/test/fixtures/pickpoint_madison_square_garden
@@ -0,0 +1,150 @@
+[
+
+    {
+        "place_id": "30632629",
+        "licence": "Data Copyright OpenStreetMap Contributors, Some Rights Reserved. CC-BY-SA 2.0.",
+        "osm_type": "way",
+        "osm_id": "24801588",
+        "boundingbox": [
+            "40.749828338623",
+            "40.7511596679688",
+            "-73.9943389892578",
+            "-73.9926528930664"
+        ],
+        "polygonpoints": [
+            [
+                "-73.9943346",
+                "40.7503638"
+            ],
+            [
+                "-73.9942745",
+                "40.7504158"
+            ],
+            [
+                "-73.9942593",
+                "40.750629"
+            ],
+            [
+                "-73.9941343",
+                "40.7508432"
+            ],
+            [
+                "-73.9939794",
+                "40.7509703"
+            ],
+            [
+                "-73.9938042",
+                "40.7510532"
+            ],
+            [
+                "-73.9938025",
+                "40.7511311"
+            ],
+            [
+                "-73.9936051",
+                "40.7511571"
+            ],
+            [
+                "-73.9935673",
+                "40.751105"
+            ],
+            [
+                "-73.9934095",
+                "40.7511089"
+            ],
+            [
+                "-73.9931235",
+                "40.7510548"
+            ],
+            [
+                "-73.9928863",
+                "40.7509311"
+            ],
+            [
+                "-73.9928068",
+                "40.750949"
+            ],
+            [
+                "-73.992721",
+                "40.7508515"
+            ],
+            [
+                "-73.9927444",
+                "40.7507889"
+            ],
+            [
+                "-73.9926693",
+                "40.7506457"
+            ],
+            [
+                "-73.9926597",
+                "40.7503657"
+            ],
+            [
+                "-73.9928305",
+                "40.7500953"
+            ],
+            [
+                "-73.9929757",
+                "40.7499911"
+            ],
+            [
+                "-73.9931281",
+                "40.7499238"
+            ],
+            [
+                "-73.993133",
+                "40.7498631"
+            ],
+            [
+                "-73.9932961",
+                "40.7498306"
+            ],
+            [
+                "-73.9933664",
+                "40.7498742"
+            ],
+            [
+                "-73.993471",
+                "40.7498701"
+            ],
+            [
+                "-73.9938023",
+                "40.7499263"
+            ],
+            [
+                "-73.9940703",
+                "40.7500756"
+            ],
+            [
+                "-73.9941876",
+                "40.7502038"
+            ],
+            [
+                "-73.9942831",
+                "40.7502142"
+            ],
+            [
+                "-73.9943346",
+                "40.7503638"
+            ]
+        ],
+        "lat": "40.7504928941818",
+        "lon": "-73.993466492276",
+        "display_name": "Madison Square Garden, West 31st Street, Long Island City, New York City, New York, 10001, United States of America",
+        "class": "leisure",
+        "type": "stadium",
+        "address": {
+            "stadium": "Madison Square Garden",
+            "road": "West 31st Street",
+            "suburb": "Long Island City",
+            "city": "New York City",
+            "county": "New York",
+            "state": "New York",
+            "postcode": "10001",
+            "country": "United States of America",
+            "country_code": "us"
+        }
+    }
+
+]
\ No newline at end of file
diff --git a/test/fixtures/pickpoint_no_results b/test/fixtures/pickpoint_no_results
new file mode 100644
index 0000000..1e3ec72
--- /dev/null
+++ b/test/fixtures/pickpoint_no_results
@@ -0,0 +1 @@
+[ ]
diff --git a/test/fixtures/pointpin_555_555_555_555 b/test/fixtures/pointpin_555_555_555_555
new file mode 100644
index 0000000..859aa41
--- /dev/null
+++ b/test/fixtures/pointpin_555_555_555_555
@@ -0,0 +1 @@
+{"error":"Invalid IP address"}
\ No newline at end of file
diff --git a/test/fixtures/pointpin_80_111_55_55 b/test/fixtures/pointpin_80_111_55_55
new file mode 100644
index 0000000..2ec552d
--- /dev/null
+++ b/test/fixtures/pointpin_80_111_55_55
@@ -0,0 +1 @@
+{"ip":"80.111.555.555","continent_code":"EU","country_code":"IE","country_name":"Ireland","region_name":"Dublin City","region_code":"D8","city_name":"Dublin","postcode":"8","latitude":53.3331,"longitude":-6.2489,"time_zone":"Europe/Dublin"}
\ No newline at end of file
diff --git a/test/fixtures/pointpin_8_8_8_8 b/test/fixtures/pointpin_8_8_8_8
new file mode 100644
index 0000000..2ddd884
--- /dev/null
+++ b/test/fixtures/pointpin_8_8_8_8
@@ -0,0 +1 @@
+{"error":"Address not found"}
\ No newline at end of file
diff --git a/test/fixtures/pointpin_no_results b/test/fixtures/pointpin_no_results
new file mode 100644
index 0000000..2ddd884
--- /dev/null
+++ b/test/fixtures/pointpin_no_results
@@ -0,0 +1 @@
+{"error":"Address not found"}
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_WR26NJ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_WR26NJ
new file mode 100644
index 0000000..3223468
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_WR26NJ
@@ -0,0 +1 @@
+[{"Location":"Moseley Road, Hallow, Worcester","Easting":"381676","Northing":"259425","Latitude":"52.2327","Longitude":"-2.2697","OsGrid":"SO 81676 59425","Accuracy":"Standard"}]
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_generic_error b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_generic_error
new file mode 100644
index 0000000..4dd3044
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_generic_error
@@ -0,0 +1 @@
+[{"Error":"9999","Description":"A generic error","Cause":"A generic error occured.","Resolution":"Fix the unknown error."}]
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_hampshire b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_hampshire
new file mode 100644
index 0000000..6eb2a4a
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_hampshire
@@ -0,0 +1 @@
+[{"Location":"Hampshire","Easting":"448701","Northing":"126642","Latitude":"51.037","Longitude":"-1.3068","OsGrid":"SU 48701 26642","Accuracy":"Standard"}]
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_key_limit_exceeded b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_key_limit_exceeded
new file mode 100644
index 0000000..479b869
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_key_limit_exceeded
@@ -0,0 +1 @@
+[{"Error":"8","Description":"Key daily limit exceeded","Cause":"The daily limit on the key has been exceeded.","Resolution":"Alter the daily limit on the key. Check the usage details first to see if usage is normal."}]
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_no_results b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_no_results
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_no_results
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_romsey b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_romsey
new file mode 100644
index 0000000..340b18a
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_romsey
@@ -0,0 +1 @@
+[{"Location":"Romsey, Hampshire","Easting":"435270","Northing":"121182","Latitude":"50.9889","Longitude":"-1.4989","OsGrid":"SU 35270 21182","Accuracy":"Standard"}]
\ No newline at end of file
diff --git a/test/fixtures/postcode_anywhere_uk_geocode_v2_00_unknown_key b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_unknown_key
new file mode 100644
index 0000000..091c3a6
--- /dev/null
+++ b/test/fixtures/postcode_anywhere_uk_geocode_v2_00_unknown_key
@@ -0,0 +1 @@
+[{"Error":"2","Description":"Unknown key","Cause":"The key you are using to access the service was not found.","Resolution":"Please check that the key is correct. It should be in the form AA11-AA11-AA11-AA11."}]
\ No newline at end of file
diff --git a/test/fixtures/postcodes_io_malvern_hills b/test/fixtures/postcodes_io_malvern_hills
new file mode 100644
index 0000000..7a43cfc
--- /dev/null
+++ b/test/fixtures/postcodes_io_malvern_hills
@@ -0,0 +1,36 @@
+{
+    "status": 200,
+    "result": {
+        "postcode": "WR2 6NJ",
+        "quality": 1,
+        "eastings": 381676,
+        "northings": 259425,
+        "country": "England",
+        "nhs_ha": "West Midlands",
+        "longitude": -2.26972239639173,
+        "latitude": 52.2327158260535,
+        "european_electoral_region": "West Midlands",
+        "primary_care_trust": "Worcestershire",
+        "region": "West Midlands",
+        "lsoa": "Malvern Hills 002B",
+        "msoa": "Malvern Hills 002",
+        "incode": "6NJ",
+        "outcode": "WR2",
+        "parliamentary_constituency": "West Worcestershire",
+        "admin_district": "Malvern Hills",
+        "parish": "Hallow",
+        "admin_county": "Worcestershire",
+        "admin_ward": "Hallow",
+        "ccg": "NHS South Worcestershire",
+        "nuts": "Worcestershire",
+        "codes": {
+            "admin_district": "E07000235",
+            "admin_county": "E10000034",
+            "admin_ward": "E05007851",
+            "parish": "E04010305",
+            "parliamentary_constituency": "E14001035",
+            "ccg": "E38000166",
+            "nuts": "UKG12"
+        }
+    }
+}
diff --git a/test/fixtures/postcodes_io_no_results b/test/fixtures/postcodes_io_no_results
new file mode 100644
index 0000000..0c929cd
--- /dev/null
+++ b/test/fixtures/postcodes_io_no_results
@@ -0,0 +1,4 @@
+{
+    "status": 404,
+    "error": "Postcode not found"
+}
diff --git a/test/fixtures/smarty_streets_10300 b/test/fixtures/smarty_streets_10300
new file mode 100644
index 0000000..2b5ed62
--- /dev/null
+++ b/test/fixtures/smarty_streets_10300
@@ -0,0 +1 @@
+{"input_index":0, "status":"invalid_zipcode", "reason":"Invalid ZIP Code."}
diff --git a/test/fixtures/smarty_streets_11211 b/test/fixtures/smarty_streets_11211
new file mode 100644
index 0000000..da5745e
--- /dev/null
+++ b/test/fixtures/smarty_streets_11211
@@ -0,0 +1 @@
+[{"input_index":0,"city_states":[{"city":"Brooklyn","state_abbreviation":"NY","state":"New York"}],"zipcodes":[{"zipcode":"11211","zipcode_type":"S","county_fips":"36047","county_name":"Kings","latitude":40.71184,"longitude":-73.95288}]}]
\ No newline at end of file
diff --git a/test/fixtures/smarty_streets_13_rue_yves_toudic_75010 b/test/fixtures/smarty_streets_13_rue_yves_toudic_75010
new file mode 100644
index 0000000..9948ad4
--- /dev/null
+++ b/test/fixtures/smarty_streets_13_rue_yves_toudic_75010
@@ -0,0 +1,33 @@
+[
+  {
+    "address1": "13 Rue Yves Toudic",
+    "address2": "10E Arrondissement",
+    "address3": "75010 Paris",
+    "components": {
+      "super_administrative_area": "Ile-De-France",
+      "administrative_area": "Paris",
+      "country_iso_3": "FRA",
+      "locality": "Paris",
+      "dependent_locality": "10E Arrondissement",
+      "postal_code": "75010",
+      "postal_code_short": "75010",
+      "premise": "13",
+      "premise_number": "13",
+      "thoroughfare": "Rue Yves Toudic",
+      "thoroughfare_name": "Yves Toudic",
+      "thoroughfare_type": "Rue"
+    },
+    "metadata": {
+      "latitude": 48.870131,
+      "longitude": 2.363473,
+      "geocode_precision": "Premise",
+      "max_geocode_precision": "DeliveryPoint",
+      "address_format": "premise thoroughfare|dependent_locality|postal_code locality"
+    },
+    "analysis": {
+      "verification_status": "Verified",
+      "address_precision": "Premise",
+      "max_address_precision": "DeliveryPoint"
+    }
+  }
+]
diff --git a/test/fixtures/smarty_streets_96628 b/test/fixtures/smarty_streets_96628
new file mode 100644
index 0000000..e2da38b
--- /dev/null
+++ b/test/fixtures/smarty_streets_96628
@@ -0,0 +1 @@
+[{"input_index":0,"city_states":[{"city":"FPO","state_abbreviation":"AP","state":"ArmedForcesPacific","mailable_city":true}],"zipcodes":[{"zipcode":"96628","zipcode_type":"M","default_city":"Fpo","county_fips":"00000","county_name":"None","state_abbreviation":"AP","state":"ArmedForcesPacific","precision":"None"}]}]
diff --git a/test/fixtures/smarty_streets_madison_square_garden b/test/fixtures/smarty_streets_madison_square_garden
new file mode 100644
index 0000000..fecaa48
--- /dev/null
+++ b/test/fixtures/smarty_streets_madison_square_garden
@@ -0,0 +1,47 @@
+[
+  {
+    "input_index": 0,
+    "candidate_index": 0,
+    "addressee": "Madison Sq Garden",
+    "delivery_line_1": "2 Penn Plz Fl 15",
+    "last_line": "New York NY 10121-1703",
+    "delivery_point_barcode": "101211703022",
+    "components": {
+      "primary_number": "2",
+      "street_name": "Penn",
+      "street_suffix": "Plz",
+      "secondary_number": "15",
+      "secondary_designator": "Fl",
+      "city_name": "New York",
+      "state_abbreviation": "NY",
+      "zipcode": "10121",
+      "plus4_code": "1703",
+      "delivery_point": "02",
+      "delivery_point_check_digit": "2"
+    },
+    "metadata": {
+      "record_type": "F",
+      "zip_type": "Standard",
+      "county_fips": "36061",
+      "county_name": "New York",
+      "carrier_route": "C038",
+      "congressional_district": "12",
+      "rdi": "Commercial",
+      "elot_sequence": "0035",
+      "elot_sort": "A",
+      "latitude": 40.74959,
+      "longitude": -73.99251,
+      "precision": "Zip7",
+      "time_zone": "Eastern",
+      "utc_offset": -5.0,
+      "dst": true
+    },
+    "analysis": {
+      "dpv_match_code": "Y",
+      "dpv_footnotes": "AABB",
+      "dpv_cmra": "N",
+      "dpv_vacant": "N",
+      "active": "Y"
+    }
+  }
+]
diff --git a/test/fixtures/smarty_streets_no_results b/test/fixtures/smarty_streets_no_results
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/test/fixtures/smarty_streets_no_results
@@ -0,0 +1 @@
+[]
diff --git a/test/fixtures/telize_555_555_555_555 b/test/fixtures/telize_555_555_555_555
new file mode 100644
index 0000000..b85a559
--- /dev/null
+++ b/test/fixtures/telize_555_555_555_555
@@ -0,0 +1,4 @@
+{
+"message": "Input string is not a valid IP address",
+"code": 401
+}
diff --git a/test/fixtures/telize_74_200_247_59 b/test/fixtures/telize_74_200_247_59
new file mode 100644
index 0000000..522e013
--- /dev/null
+++ b/test/fixtures/telize_74_200_247_59
@@ -0,0 +1,17 @@
+{
+  "longitude": -74.0468,
+  "city": "Jersey City",
+  "timezone": "America/New_York",
+  "latitude": 40.7209,
+  "asn": 22576,
+  "region": "New Jersey",
+  "offset": -14400,
+  "organization": "DataPipe, Inc.",
+  "country_code": "US",
+  "ip": "74.200.247.59",
+  "country_code3": "USA",
+  "postal_code": "07302",
+  "continent_code": "NA",
+  "country": "United States",
+  "region_code": "NJ"
+}
diff --git a/test/fixtures/telize_8_8_8_8 b/test/fixtures/telize_8_8_8_8
new file mode 100644
index 0000000..e1cbeb5
--- /dev/null
+++ b/test/fixtures/telize_8_8_8_8
@@ -0,0 +1 @@
+{"ip":"8.8.8.8"}
diff --git a/test/fixtures/telize_no_results b/test/fixtures/telize_no_results
new file mode 100644
index 0000000..e1cbeb5
--- /dev/null
+++ b/test/fixtures/telize_no_results
@@ -0,0 +1 @@
+{"ip":"8.8.8.8"}
diff --git a/test/fixtures/tencent_invalid_key b/test/fixtures/tencent_invalid_key
new file mode 100644
index 0000000..f5dd70f
--- /dev/null
+++ b/test/fixtures/tencent_invalid_key
@@ -0,0 +1,4 @@
+{
+    "status":311,
+    "message":"key格式错误"
+}
diff --git a/test/fixtures/tencent_no_results b/test/fixtures/tencent_no_results
new file mode 100644
index 0000000..178fff9
--- /dev/null
+++ b/test/fixtures/tencent_no_results
@@ -0,0 +1,4 @@
+{
+    "status":347,
+    "message":"查询无结果"
+}
diff --git a/test/fixtures/tencent_reverse b/test/fixtures/tencent_reverse
new file mode 100644
index 0000000..268376f
--- /dev/null
+++ b/test/fixtures/tencent_reverse
@@ -0,0 +1,110 @@
+{
+    "status":0,
+    "message":"query ok",
+    "request_id":"b8580ff8-b385-11e8-bb66-246e965de502",
+    "result":{
+        "location":{
+            "lat":31.239664,
+            "lng":121.499809
+        },
+        "address":"上海市浦东新区世纪大道1号",
+        "formatted_addresses":{
+            "recommend":"浦东新区东方明珠广播电视塔",
+            "rough":"浦东新区东方明珠广播电视塔"
+        },
+        "address_component":{
+            "nation":"中国",
+            "province":"上海市",
+            "city":"上海市",
+            "district":"浦东新区",
+            "street":"世纪大道",
+            "street_number":"世纪大道1号"
+        },
+        "ad_info":{
+            "nation_code":"156",
+            "adcode":"310115",
+            "city_code":"156310000",
+            "name":"中国,上海市,上海市,浦东新区",
+            "location":{
+                "lat":31.239664,
+                "lng":121.499809
+            },
+            "nation":"中国",
+            "province":"上海市",
+            "city":"上海市",
+            "district":"浦东新区"
+        },
+        "address_reference":{
+            "business_area":{
+                "id":"17883725287589114835",
+                "title":"陆家嘴",
+                "location":{
+                    "lat":31.239664,
+                    "lng":121.499809
+                },
+                "_distance":0,
+                "_dir_desc":"内"
+            },
+            "famous_area":{
+                "id":"17883725287589114835",
+                "title":"陆家嘴",
+                "location":{
+                    "lat":31.239664,
+                    "lng":121.499809
+                },
+                "_distance":0,
+                "_dir_desc":"内"
+            },
+            "crossroad":{
+                "id":"5571681",
+                "title":"丰和路/明珠塔路(路口)",
+                "location":{
+                    "lat":31.23901,
+                    "lng":121.499138
+                },
+                "_distance":91.4,
+                "_dir_desc":"东北"
+            },
+            "town":{
+                "id":"310115005",
+                "title":"陆家嘴街道",
+                "location":{
+                    "lat":31.239664,
+                    "lng":121.499809
+                },
+                "_distance":0,
+                "_dir_desc":"内"
+            },
+            "street_number":{
+                "id":"15588308480676188216",
+                "title":"世纪大道1号",
+                "location":{
+                    "lat":31.239777,
+                    "lng":121.49968
+                },
+                "_distance":17.6,
+                "_dir_desc":""
+            },
+            "street":{
+                "id":"9311677944217128536",
+                "title":"明珠塔路",
+                "location":{
+                    "lat":31.23901,
+                    "lng":121.499138
+                },
+                "_distance":91.4,
+                "_dir_desc":"北"
+            },
+            "landmark_l2":{
+                "id":"15588308480676188216",
+                "title":"东方明珠广播电视塔",
+                "location":{
+                    "lat":31.239691,
+                    "lng":121.499718
+                },
+                "_distance":0,
+                "_dir_desc":"内"
+            }
+        }
+    }
+}
diff --git a/test/fixtures/tencent_shanghai_pearl_tower b/test/fixtures/tencent_shanghai_pearl_tower
new file mode 100644
index 0000000..a4f3124
--- /dev/null
+++ b/test/fixtures/tencent_shanghai_pearl_tower
@@ -0,0 +1,22 @@
+{
+    "status":0,
+    "message":"query ok",
+    "result":{
+        "title":"东方明珠主观光层",
+        "location":{
+            "lng":121.499809,
+            "lat":31.239664
+        },
+        "address_components":{
+            "province":"上海市",
+            "city":"上海市",
+            "district":"浦东新区",
+            "street":"",
+            "street_number":""
+        },
+        "similarity":0.8,
+        "deviation":1000,
+        "reliability":7,
+        "level":11
+    }
+}
\ No newline at end of file
diff --git a/test/fixtures/twogis_chernoe_more b/test/fixtures/twogis_chernoe_more
new file mode 100644
index 0000000..fd5f5f4
--- /dev/null
+++ b/test/fixtures/twogis_chernoe_more
@@ -0,0 +1,108 @@
+{
+   "meta":{
+      "api_version":"3.0.783441",
+      "code":200,
+      "issue_date":"20220406"
+   },
+   "result":{
+      "items":[
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447163",
+                  "name":"Московская область",
+                  "type":"region"
+               },
+               {
+                  "id":"1267651007479930",
+                  "name":"Балашиха городской округ",
+                  "type":"district_area"
+               }
+            ],
+            "full_name":"Черное",
+            "geometry":{
+               "centroid":"POINT(38.070341 55.748297)"
+            },
+            "id":"4504222397629494",
+            "name":"Черное",
+            "point":{
+               "lat":55.748297,
+               "lon":38.070341
+            },
+            "subtype":"settlement",
+            "subtype_specification":"Деревня",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447179",
+                  "name":"Смоленская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118167848",
+                  "name":"Вяземский район",
+                  "type":"district_area"
+               }
+            ],
+            "full_name":"Черное",
+            "geometry":{
+               "centroid":"POINT(33.992999 55.193532)"
+            },
+            "id":"70030076128006108",
+            "name":"Черное",
+            "point":{
+               "lat":55.193532,
+               "lon":33.992999
+            },
+            "subtype":"settlement",
+            "subtype_specification":"Деревня",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447184",
+                  "name":"Тюменская область",
+                  "type":"region"
+               },
+               {
+                  "id":"4786088216363089",
+                  "name":"Вагайский район",
+                  "type":"district_area"
+               }
+            ],
+            "full_name":"Черное",
+            "geometry":{
+               "centroid":"POINT(69.169569 57.65249)"
+            },
+            "id":"70030076128084819",
+            "name":"Черное",
+            "point":{
+               "lat":57.65249,
+               "lon":69.169569
+            },
+            "subtype":"settlement",
+            "subtype_specification":"Село",
+            "type":"adm_div"
+         }
+      ],
+      "total":3
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/twogis_kremlin b/test/fixtures/twogis_kremlin
new file mode 100644
index 0000000..31649eb
--- /dev/null
+++ b/test/fixtures/twogis_kremlin
@@ -0,0 +1,67 @@
+{
+   "meta":{
+      "api_version":"3.0.783441",
+      "code":200,
+      "issue_date":"20220406"
+   },
+   "result":{
+      "items":[
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"5349042514588558",
+                  "name":"Москва",
+                  "type":"region"
+               }
+            ],
+            "city_alias":"moscow",
+            "full_name":"Москва",
+            "geometry":{
+               "centroid":"POINT(37.617774 55.755836)"
+            },
+            "id":"4504222397630173",
+            "name":"Москва",
+            "point":{
+               "lat":55.755836,
+               "lon":37.617774
+            },
+            "subtype":"city",
+            "type":"adm_div"
+         },
+         {
+            "full_name":"Moscow",
+            "geometry":{
+               "centroid":"POINT(37.615773 55.739711)"
+            },
+            "id":"5349042514588558",
+            "name":"Moscow",
+            "point":{
+               "lat":55.739711,
+               "lon":37.615773
+            },
+            "subtype":"region",
+            "type":"adm_div"
+         },
+         {
+            "full_name":"Moscow region",
+            "geometry":{
+               "centroid":"POINT(37.48821 56.138265)"
+            },
+            "id":"1267655302447163",
+            "name":"Moscow region",
+            "point":{
+               "lat":56.138265,
+               "lon":37.48821
+            },
+            "subtype":"region",
+            "type":"adm_div"
+         }
+      ],
+      "total":3
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/twogis_new_york b/test/fixtures/twogis_new_york
new file mode 100644
index 0000000..8948cd9
--- /dev/null
+++ b/test/fixtures/twogis_new_york
@@ -0,0 +1,53 @@
+{
+   "meta":{
+      "api_version":"3.0.783441",
+      "code":200,
+      "issue_date":"20220406"
+   },
+   "result":{
+      "items":[
+         {
+            "adm_div":[
+               {
+                  "id":"70030076149193895",
+                  "name":"Abu Dhabi Emirate",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076170335813",
+                  "name":"Al Dhafra Municipality",
+                  "type":"district_area"
+               }
+            ],
+            "full_name":"Ruwais New",
+            "geometry":{
+               "centroid":"POINT(52.883827 24.120249)"
+            },
+            "id":"70030076164433621",
+            "name":"Ruwais New",
+            "point":{
+               "lat":24.120249,
+               "lon":52.883827
+            },
+            "subtype":"settlement",
+            "subtype_specification":"settlement",
+            "type":"adm_div"
+         },
+         {
+            "full_name":"New Valley Governorate",
+            "geometry":{
+               "centroid":"POINT(28.149757 24.908295)"
+            },
+            "id":"70030076149052626",
+            "name":"New Valley Governorate",
+            "point":{
+               "lat":24.908295,
+               "lon":28.149757
+            },
+            "subtype":"region",
+            "type":"adm_div"
+         }
+      ],
+      "total":2
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/twogis_no_results b/test/fixtures/twogis_no_results
new file mode 100644
index 0000000..011a02e
--- /dev/null
+++ b/test/fixtures/twogis_no_results
@@ -0,0 +1,11 @@
+{
+   "meta":{
+      "api_version":"3.0.783441",
+      "code":404,
+      "error":{
+         "message":"Results not found",
+         "type":"itemNotFound"
+      },
+      "issue_date":"20220406"
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/twogis_ohotniy_riad_2 b/test/fixtures/twogis_ohotniy_riad_2
new file mode 100644
index 0000000..bb65815
--- /dev/null
+++ b/test/fixtures/twogis_ohotniy_riad_2
@@ -0,0 +1,103 @@
+{
+   "meta":{
+      "api_version":"3.0.783441",
+      "code":200,
+      "issue_date":"20220406"
+   },
+   "result":{
+      "items":[
+         {
+            "address_name":"улица Охотный Ряд, 2",
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"5349042514588558",
+                  "name":"Москва",
+                  "type":"region"
+               },
+               {
+                  "city_alias":"moscow",
+                  "flags":{
+                     "is_default":true,
+                     "is_region_center":true
+                  },
+                  "id":"4504222397630173",
+                  "is_default":true,
+                  "name":"Москва",
+                  "type":"city"
+               },
+               {
+                  "id":"4504209512726536",
+                  "name":"Тверской район",
+                  "type":"district"
+               }
+            ],
+            "building_name":"Four Seasons Moscow, отель",
+            "city_alias":"moscow",
+            "full_address_name":"Москва, улица Охотный Ряд, 2",
+            "full_name":"Москва, Four Seasons Moscow, отель",
+            "geometry":{
+               "centroid":"POINT(37.616732 55.757261)"
+            },
+            "id":"4504235282810606",
+            "name":"Four Seasons Moscow, отель",
+            "point":{
+               "lat":55.757261,
+               "lon":37.616732
+            },
+            "purpose_name":"Многофункциональный комплекс",
+            "type":"building"
+         },
+         {
+            "address_name":"улица Охотный Ряд, вл2",
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"5349042514588558",
+                  "name":"Москва",
+                  "type":"region"
+               },
+               {
+                  "city_alias":"moscow",
+                  "flags":{
+                     "is_default":true,
+                     "is_region_center":true
+                  },
+                  "id":"4504222397630173",
+                  "is_default":true,
+                  "name":"Москва",
+                  "type":"city"
+               },
+               {
+                  "id":"4504209512726536",
+                  "name":"Тверской район",
+                  "type":"district"
+               }
+            ],
+            "city_alias":"moscow",
+            "full_address_name":"Москва, улица Охотный Ряд, вл2",
+            "full_name":"Москва, улица Охотный Ряд, вл2",
+            "geometry":{
+               "centroid":"POINT(37.618753 55.758332)"
+            },
+            "id":"70030076128378739",
+            "name":"улица Охотный Ряд, вл2",
+            "point":{
+               "lat":55.758332,
+               "lon":37.618753
+            },
+            "purpose_name":"Автостоянка",
+            "type":"building"
+         }
+      ],
+      "total":2
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/twogis_volga_river b/test/fixtures/twogis_volga_river
new file mode 100644
index 0000000..69dc201
--- /dev/null
+++ b/test/fixtures/twogis_volga_river
@@ -0,0 +1,296 @@
+{
+   "meta":{
+      "api_version":"3.0.783441",
+      "code":200,
+      "issue_date":"20220406"
+   },
+   "result":{
+      "items":[
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168103",
+                  "name":"Некоузский район",
+                  "type":"district_area"
+               }
+            ],
+            "full_name":"Волга",
+            "geometry":{
+               "centroid":"POINT(38.388873 57.953151)"
+            },
+            "id":"70030076143520494",
+            "name":"Волга",
+            "point":{
+               "lat":57.953151,
+               "lon":38.388873
+            },
+            "subtype":"settlement",
+            "subtype_specification":"Посёлок",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1267655302447157",
+                  "name":"Kostroma region",
+                  "type":"region"
+               },
+               {
+                  "id":"4786088216363088",
+                  "name":"Красносельский район",
+                  "type":"district_area"
+               }
+            ],
+            "full_name":"Красное-на-Волге",
+            "geometry":{
+               "centroid":"POINT(41.241364 57.513871)"
+            },
+            "id":"70030076128113899",
+            "name":"Красное-на-Волге",
+            "point":{
+               "lat":57.513871,
+               "lon":41.241364
+            },
+            "subtype":"settlement",
+            "subtype_specification":"Urban settlement",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168095",
+                  "name":"Ярославль городской округ",
+                  "type":"district_area"
+               },
+               {
+                  "city_alias":"yaroslavl",
+                  "flags":{
+                     "is_district_area_center":true,
+                     "is_region_center":true
+                  },
+                  "id":"3941272444207172",
+                  "name":"Ярославль",
+                  "type":"city"
+               }
+            ],
+            "city_alias":"yaroslavl",
+            "full_name":"Ярославль, Волга река",
+            "geometry":{
+               "centroid":"POINT(40.119751 57.558464)"
+            },
+            "id":"3941336868716796",
+            "name":"Волга река",
+            "point":{
+               "lat":57.558464,
+               "lon":40.119751
+            },
+            "subtype":"place",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168102",
+                  "name":"Мышкинский район",
+                  "type":"district_area"
+               },
+               {
+                  "flags":{
+                     "is_district_area_center":true
+                  },
+                  "id":"70030076128001710",
+                  "name":"Мышкин",
+                  "type":"city"
+               }
+            ],
+            "full_name":"Мышкин, СТ Волга",
+            "geometry":{
+               "centroid":"POINT(38.456843 57.799505)"
+            },
+            "id":"70030076320028338",
+            "name":"СТ Волга",
+            "point":{
+               "lat":57.799505,
+               "lon":38.456843
+            },
+            "subtype":"living_area",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168111",
+                  "name":"Угличский район",
+                  "type":"district_area"
+               },
+               {
+                  "id":"70030076127996231",
+                  "name":"д. Баскачи",
+                  "type":"settlement"
+               }
+            ],
+            "full_name":"Баскачи, СТ Волга",
+            "geometry":{
+               "centroid":"POINT(38.347757 57.580027)"
+            },
+            "id":"70030076342541258",
+            "name":"СТ Волга",
+            "point":{
+               "lat":57.580027,
+               "lon":38.347757
+            },
+            "subtype":"living_area",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168110",
+                  "name":"Тутаевский район",
+                  "type":"district_area"
+               },
+               {
+                  "id":"70030076127991633",
+                  "name":"д. Безмино",
+                  "type":"settlement"
+               }
+            ],
+            "full_name":"Безмино, река Волга",
+            "geometry":{
+               "centroid":"POINT(38.338034 58.049539)"
+            },
+            "id":"70030076245869325",
+            "name":"река Волга",
+            "point":{
+               "lat":58.049539,
+               "lon":38.338034
+            },
+            "subtype":"place",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168104",
+                  "name":"Некрасовский район",
+                  "type":"district_area"
+               },
+               {
+                  "id":"70030076127992813",
+                  "name":"д. Федорово",
+                  "type":"settlement"
+               }
+            ],
+            "full_name":"Федорово, река Волга",
+            "geometry":{
+               "centroid":"POINT(40.591411 57.7719)"
+            },
+            "id":"70030076316108375",
+            "name":"река Волга",
+            "point":{
+               "lat":57.7719,
+               "lon":40.591411
+            },
+            "subtype":"place",
+            "type":"adm_div"
+         },
+         {
+            "adm_div":[
+               {
+                  "id":"1",
+                  "name":"Россия",
+                  "type":"country"
+               },
+               {
+                  "id":"1267655302447187",
+                  "name":"Ярославская область",
+                  "type":"region"
+               },
+               {
+                  "id":"70030076118168111",
+                  "name":"Угличский район",
+                  "type":"district_area"
+               },
+               {
+                  "id":"70030076127996225",
+                  "name":"д. Житово",
+                  "type":"settlement"
+               }
+            ],
+            "full_name":"Житово, река Волга",
+            "geometry":{
+               "centroid":"POINT(38.271856 57.48617)"
+            },
+            "id":"70030076316828925",
+            "name":"река Волга",
+            "point":{
+               "lat":57.48617,
+               "lon":38.271856
+            },
+            "subtype":"place",
+            "type":"adm_div"
+         }
+      ],
+      "total":8
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/uk_ordnance_survey_names_SW1A1AA b/test/fixtures/uk_ordnance_survey_names_SW1A1AA
new file mode 100644
index 0000000..681a48d
--- /dev/null
+++ b/test/fixtures/uk_ordnance_survey_names_SW1A1AA
@@ -0,0 +1,1268 @@
+{
+    "header": {
+        "uri": "https://api.os.uk/search/names/v1/find?query=SW1a1AA&fq=local_type%3ACity%20local_type%3AHamlet%20local_type%3AOther_Settlement%20local_type%3ATown%20local_type%3AVillage%20local_type%3APostcode",
+        "query": "SW1a1AA",
+        "format": "JSON",
+        "maxresults": 100,
+        "offset": 0,
+        "totalresults": 50,
+        "filter": "fq=local_type:City local_type:Hamlet local_type:Other_Settlement local_type:Town local_type:Village local_type:Postcode"
+    },
+    "results": [
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1AA",
+                "NAME1": "SW1A 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529090,
+                "GEOMETRY_Y": 179645,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW111AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW111AA",
+                "NAME1": "SW11 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 527614,
+                "GEOMETRY_Y": 175543,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Battersea",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559558",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW151AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW151AA",
+                "NAME1": "SW15 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 523544,
+                "GEOMETRY_Y": 175403,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Putney",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074562835",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW161AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW161AA",
+                "NAME1": "SW16 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 530019,
+                "GEOMETRY_Y": 172701,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Streatham",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575698",
+                "DISTRICT_BOROUGH": "Lambeth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011144",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW181AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW181AA",
+                "NAME1": "SW18 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526140,
+                "GEOMETRY_Y": 174861,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Wandsworth",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074576254",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW191AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW191AA",
+                "NAME1": "SW19 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526211,
+                "GEOMETRY_Y": 170740,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Wimbledon",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074541243",
+                "DISTRICT_BOROUGH": "Merton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010995",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A0AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A0AA",
+                "NAME1": "SW1A 0AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 530268,
+                "GEOMETRY_Y": 179545,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1AB",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1AB",
+                "NAME1": "SW1A 1AB",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 530240,
+                "GEOMETRY_Y": 180708,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1BA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1BA",
+                "NAME1": "SW1A 1BA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529292,
+                "GEOMETRY_Y": 179988,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1DA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1DA",
+                "NAME1": "SW1A 1DA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529265,
+                "GEOMETRY_Y": 180092,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1EA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1EA",
+                "NAME1": "SW1A 1EA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529340,
+                "GEOMETRY_Y": 180172,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1HA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1HA",
+                "NAME1": "SW1A 1HA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529266,
+                "GEOMETRY_Y": 180304,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1LA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1LA",
+                "NAME1": "SW1A 1LA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529167,
+                "GEOMETRY_Y": 180313,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1RA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1RA",
+                "NAME1": "SW1A 1RA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529122,
+                "GEOMETRY_Y": 180335,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A1ZA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A1ZA",
+                "NAME1": "SW1A 1ZA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529548,
+                "GEOMETRY_Y": 177433,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1A2AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1A2AA",
+                "NAME1": "SW1A 2AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 530047,
+                "GEOMETRY_Y": 179951,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1P1AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1P1AA",
+                "NAME1": "SW1P 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529391,
+                "GEOMETRY_Y": 179143,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW1V1AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW1V1AA",
+                "NAME1": "SW1V 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 529096,
+                "GEOMETRY_Y": 179021,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "W1A1AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/W1A1AA",
+                "NAME1": "W1A 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 528887,
+                "GEOMETRY_Y": 181593,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "City of Westminster",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559881",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "CW111AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/CW111AA",
+                "NAME1": "CW11 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 375892,
+                "GEOMETRY_Y": 360810,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Sandbach",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074576355",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Cheshire East",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043553",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "North West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041431",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "EC1A1AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/EC1A1AA",
+                "NAME1": "EC1A 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 531131,
+                "GEOMETRY_Y": 182382,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Islington",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011281",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "KW151AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/KW151AA",
+                "NAME1": "KW15 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 344877,
+                "GEOMETRY_Y": 1011082,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Kirkwall",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074557613",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Orkney Islands",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000029961",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Scotland",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041429",
+                "COUNTRY": "Scotland",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/scotland"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "NW101AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/NW101AA",
+                "NAME1": "NW10 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 521839,
+                "GEOMETRY_Y": 185653,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Brent",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011447",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SA111AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SA111AA",
+                "NAME1": "SA11 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 275157,
+                "GEOMETRY_Y": 196858,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Neath / Castell-nedd",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074548341",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Castell-nedd Port Talbot - Neath Port Talbot",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000025498",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Wales",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041424",
+                "COUNTRY": "Wales",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/wales"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SA131AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SA131AA",
+                "NAME1": "SA13 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 276924,
+                "GEOMETRY_Y": 189650,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Port Talbot",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549084",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Castell-nedd Port Talbot - Neath Port Talbot",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000025498",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Wales",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041424",
+                "COUNTRY": "Wales",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/wales"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SA151AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SA151AA",
+                "NAME1": "SA15 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 250541,
+                "GEOMETRY_Y": 200199,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Llanelli",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074571435",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Sir Gaerfyrddin - Carmarthenshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000025486",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Wales",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041424",
+                "COUNTRY": "Wales",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/wales"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SE151AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SE151AA",
+                "NAME1": "SE15 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 534257,
+                "GEOMETRY_Y": 177086,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Southwark",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011013",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SE171AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SE171AA",
+                "NAME1": "SE17 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 532610,
+                "GEOMETRY_Y": 178438,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Southwark",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011013",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SE181AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SE181AA",
+                "NAME1": "SE18 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 544850,
+                "GEOMETRY_Y": 178228,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Greenwich",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010777",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SE191AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SE191AA",
+                "NAME1": "SE19 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 533176,
+                "GEOMETRY_Y": 170843,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Lambeth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011144",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SG111AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SG111AA",
+                "NAME1": "SG11 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 536285,
+                "GEOMETRY_Y": 218516,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "High Cross",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074574014",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#village",
+                "DISTRICT_BOROUGH": "East Hertfordshire",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000003679",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Hertfordshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000003909",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SG191AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SG191AA",
+                "NAME1": "SG19 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 517251,
+                "GEOMETRY_Y": 249189,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Sandy",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074570512",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Central Bedfordshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043870",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SK101AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SK101AA",
+                "NAME1": "SK10 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 391487,
+                "GEOMETRY_Y": 373814,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Macclesfield",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074577382",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Cheshire East",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043553",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "North West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041431",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SK121AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SK121AA",
+                "NAME1": "SK12 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 391902,
+                "GEOMETRY_Y": 383726,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Poynton",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074544996",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Cheshire East",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043553",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "North West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041431",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SK131AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SK131AA",
+                "NAME1": "SK13 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 402319,
+                "GEOMETRY_Y": 396125,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Hadfield",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813565",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#village",
+                "DISTRICT_BOROUGH": "High Peak",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000013755",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Derbyshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000013688",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "East Midlands",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041423",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SK141AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SK141AA",
+                "NAME1": "SK14 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 394871,
+                "GEOMETRY_Y": 395016,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Hyde",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074545001",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "DISTRICT_BOROUGH": "Tameside",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000018700",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/MetropolitanDistrict",
+                "REGION": "North West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041431",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SK151AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SK151AA",
+                "NAME1": "SK15 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 396517,
+                "GEOMETRY_Y": 398691,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Stalybridge",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074565101",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "DISTRICT_BOROUGH": "Tameside",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000018700",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/MetropolitanDistrict",
+                "REGION": "North West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041431",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SN151AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SN151AA",
+                "NAME1": "SN15 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 391958,
+                "GEOMETRY_Y": 174073,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Chippenham",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074571472",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Wiltshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043925",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "South West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041427",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SP101AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SP101AA",
+                "NAME1": "SP10 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 436302,
+                "GEOMETRY_Y": 145363,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Andover",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074567982",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "DISTRICT_BOROUGH": "Test Valley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043511",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Hampshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000017765",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "South East",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041421",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SS141AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SS141AA",
+                "NAME1": "SS14 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 570558,
+                "GEOMETRY_Y": 188641,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Basildon",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074573659",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "DISTRICT_BOROUGH": "Basildon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000019739",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Essex",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000019687",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SS171AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SS171AA",
+                "NAME1": "SS17 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 569923,
+                "GEOMETRY_Y": 183552,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Stanford-le-Hope",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074573320",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "COUNTY_UNITARY": "Thurrock",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000038866",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "ST101AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/ST101AA",
+                "NAME1": "ST10 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 400909,
+                "GEOMETRY_Y": 343400,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Cheadle",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074560862",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "DISTRICT_BOROUGH": "Staffordshire Moorlands",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000015068",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Staffordshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000015052",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "West Midlands",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041426",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "ST161AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/ST161AA",
+                "NAME1": "ST16 1AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 391804,
+                "GEOMETRY_Y": 322841,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Stafford",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074560217",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#town",
+                "DISTRICT_BOROUGH": "Stafford",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000014892",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Staffordshire",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000015052",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "West Midlands",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041426",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW100AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW100AA",
+                "NAME1": "SW10 0AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526506,
+                "GEOMETRY_Y": 176966,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Hammersmith and Fulham",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011259",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW101AH",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW101AH",
+                "NAME1": "SW10 1AH",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526217,
+                "GEOMETRY_Y": 177763,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Kensington",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074558539",
+                "DISTRICT_BOROUGH": "Kensington and Chelsea",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011270",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW101AS",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW101AS",
+                "NAME1": "SW10 1AS",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526217,
+                "GEOMETRY_Y": 177763,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Kensington",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074558539",
+                "DISTRICT_BOROUGH": "Kensington and Chelsea",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011270",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW101AW",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW101AW",
+                "NAME1": "SW10 1AW",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526217,
+                "GEOMETRY_Y": 177763,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Kensington",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074558539",
+                "DISTRICT_BOROUGH": "Kensington and Chelsea",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011270",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW109AA",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW109AA",
+                "NAME1": "SW10 9AA",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526000,
+                "GEOMETRY_Y": 177635,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Kensington and Chelsea",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011270",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW111AB",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW111AB",
+                "NAME1": "SW11 1AB",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 527614,
+                "GEOMETRY_Y": 175543,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "Battersea",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559558",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "SW111AD",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/postcodeunit/SW111AD",
+                "NAME1": "SW11 1AD",
+                "TYPE": "other",
+                "LOCAL_TYPE": "Postcode",
+                "GEOMETRY_X": 526994,
+                "GEOMETRY_Y": 175265,
+                "MOST_DETAIL_VIEW_RES": 3500,
+                "LEAST_DETAIL_VIEW_RES": 18000,
+                "POPULATED_PLACE": "London",
+                "POPULATED_PLACE_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "POPULATED_PLACE_TYPE": "http://www.ordnancesurvey.co.uk/xml/codelists/localtype.xml#city",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        }
+    ]
+}
diff --git a/test/fixtures/uk_ordnance_survey_names_london b/test/fixtures/uk_ordnance_survey_names_london
new file mode 100644
index 0000000..1881b2f
--- /dev/null
+++ b/test/fixtures/uk_ordnance_survey_names_london
@@ -0,0 +1,3044 @@
+{
+    "header": {
+        "uri": "https://api.os.uk/search/names/v1/find?query=London&fq=local_type%3ACity%20local_type%3AHamlet%20local_type%3AOther_Settlement%20local_type%3ATown%20local_type%3AVillage%20local_type%3APostcode",
+        "query": "London",
+        "format": "JSON",
+        "maxresults": 100,
+        "offset": 0,
+        "totalresults": 211,
+        "filter": "fq=local_type:City local_type:Hamlet local_type:Other_Settlement local_type:Town local_type:Village local_type:Postcode"
+    },
+    "results": [
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343985",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343985",
+                "NAME1": "City of London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "City",
+                "GEOMETRY_X": 532473,
+                "GEOMETRY_Y": 181219,
+                "MOST_DETAIL_VIEW_RES": 19000,
+                "LEAST_DETAIL_VIEW_RES": 9000000,
+                "MBR_XMIN": 530968,
+                "MBR_YMIN": 180398,
+                "MBR_XMAX": 533841,
+                "MBR_YMAX": 182198,
+                "POSTCODE_DISTRICT": "EC2V",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/EC2V",
+                "DISTRICT_BOROUGH": "City and County of the City of London",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011105",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2643741",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/City_of_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074813508",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813508",
+                "NAME1": "London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "City",
+                "GEOMETRY_X": 530034,
+                "GEOMETRY_Y": 180381,
+                "MOST_DETAIL_VIEW_RES": 349000,
+                "LEAST_DETAIL_VIEW_RES": 9000000,
+                "MBR_XMIN": 504454,
+                "MBR_YMIN": 156590,
+                "MBR_XMAX": 558150,
+                "MBR_YMAX": 200042,
+                "POSTCODE_DISTRICT": "WC2N",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/WC2N",
+                "DISTRICT_BOROUGH": "City of Westminster",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011164",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074813722",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074813722",
+                "NAME1": "Chelsfield",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 548252,
+                "GEOMETRY_Y": 164175,
+                "MOST_DETAIL_VIEW_RES": 6000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 547768,
+                "MBR_YMIN": 163894,
+                "MBR_XMAX": 548712,
+                "MBR_YMAX": 164415,
+                "POSTCODE_DISTRICT": "BR6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR6",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074576660",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074576660",
+                "NAME1": "Coldblow",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 550399,
+                "GEOMETRY_Y": 173115,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 549931,
+                "MBR_YMIN": 172851,
+                "MBR_XMAX": 550764,
+                "MBR_YMAX": 173640,
+                "POSTCODE_DISTRICT": "DA5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/DA5",
+                "DISTRICT_BOROUGH": "Bexley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010759",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074564240",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074564240",
+                "NAME1": "Cudham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 544691,
+                "GEOMETRY_Y": 159466,
+                "MOST_DETAIL_VIEW_RES": 9000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 544368,
+                "MBR_YMIN": 158661,
+                "MBR_XMAX": 545149,
+                "MBR_YMAX": 160099,
+                "POSTCODE_DISTRICT": "TN14",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TN14",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2651779",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Cudham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074564565",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074564565",
+                "NAME1": "Downe",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 543173,
+                "GEOMETRY_Y": 161647,
+                "MOST_DETAIL_VIEW_RES": 9000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 542664,
+                "MBR_YMIN": 160956,
+                "MBR_XMAX": 543685,
+                "MBR_YMAX": 162308,
+                "POSTCODE_DISTRICT": "BR6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR6",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2651033",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Downe"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074339827",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074339827",
+                "NAME1": "Harefield",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 505235,
+                "GEOMETRY_Y": 190598,
+                "MOST_DETAIL_VIEW_RES": 17000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 503985,
+                "MBR_YMIN": 189776,
+                "MBR_XMAX": 506559,
+                "MBR_YMAX": 191754,
+                "POSTCODE_DISTRICT": "UB9",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB9",
+                "DISTRICT_BOROUGH": "Hillingdon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011539",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074564241",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074564241",
+                "NAME1": "Hazelwood",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 544729,
+                "GEOMETRY_Y": 161589,
+                "MOST_DETAIL_VIEW_RES": 7000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 544293,
+                "MBR_YMIN": 161137,
+                "MBR_XMAX": 545223,
+                "MBR_YMAX": 162253,
+                "POSTCODE_DISTRICT": "TN14",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TN14",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074549606",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549606",
+                "NAME1": "Keston",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 541242,
+                "GEOMETRY_Y": 164528,
+                "MOST_DETAIL_VIEW_RES": 19000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 540811,
+                "MBR_YMIN": 162042,
+                "MBR_XMAX": 542564,
+                "MBR_YMAX": 164993,
+                "POSTCODE_DISTRICT": "BR2",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR2",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2645758",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Keston"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074558121",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074558121",
+                "NAME1": "London Apprentice",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 200698,
+                "GEOMETRY_Y": 49932,
+                "MOST_DETAIL_VIEW_RES": 3000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 200571,
+                "MBR_YMIN": 49832,
+                "MBR_XMAX": 201071,
+                "MBR_YMAX": 50332,
+                "POSTCODE_DISTRICT": "PL26",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/PL26",
+                "COUNTY_UNITARY": "Cornwall",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000043750",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "South West",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041427",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/London_Apprentice"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074564243",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074564243",
+                "NAME1": "Luxted",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 543324,
+                "GEOMETRY_Y": 160257,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 542941,
+                "MBR_YMIN": 160071,
+                "MBR_XMAX": 543770,
+                "MBR_YMAX": 160589,
+                "POSTCODE_DISTRICT": "BR6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR6",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074318884",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074318884",
+                "NAME1": "Maypole",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 549164,
+                "GEOMETRY_Y": 163899,
+                "MOST_DETAIL_VIEW_RES": 4000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 548762,
+                "MBR_YMIN": 163576,
+                "MBR_XMAX": 549384,
+                "MBR_YMAX": 164076,
+                "POSTCODE_DISTRICT": "BR6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR6",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074550341",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074550341",
+                "NAME1": "Ruxley",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 547971,
+                "GEOMETRY_Y": 170542,
+                "MOST_DETAIL_VIEW_RES": 6000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 547808,
+                "MBR_YMIN": 170046,
+                "MBR_XMAX": 548734,
+                "MBR_YMAX": 170683,
+                "POSTCODE_DISTRICT": "DA14",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/DA14",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Ruxley"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074569525",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074569525",
+                "NAME1": "Wennington",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Village",
+                "GEOMETRY_X": 554128,
+                "GEOMETRY_Y": 180954,
+                "MOST_DETAIL_VIEW_RES": 7000,
+                "LEAST_DETAIL_VIEW_RES": 250000,
+                "MBR_XMIN": 553691,
+                "MBR_YMIN": 180709,
+                "MBR_XMAX": 554762,
+                "MBR_YMAX": 181209,
+                "POSTCODE_DISTRICT": "RM13",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/RM13",
+                "DISTRICT_BOROUGH": "Havering",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010807",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Wennington,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074548838",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074548838",
+                "NAME1": "Bopeep",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 549085,
+                "GEOMETRY_Y": 163634,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 548837,
+                "MBR_YMIN": 163328,
+                "MBR_XMAX": 549337,
+                "MBR_YMAX": 163828,
+                "POSTCODE_DISTRICT": "BR6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR6",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074549966",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549966",
+                "NAME1": "Hockenden",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 549654,
+                "GEOMETRY_Y": 169193,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 549211,
+                "MBR_YMIN": 168597,
+                "MBR_XMAX": 549894,
+                "MBR_YMAX": 169266,
+                "POSTCODE_DISTRICT": "BR8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR8",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074549232",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549232",
+                "NAME1": "Kevingtown",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 548519,
+                "GEOMETRY_Y": 167454,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 548316,
+                "MBR_YMIN": 167275,
+                "MBR_XMAX": 549075,
+                "MBR_YMAX": 168165,
+                "POSTCODE_DISTRICT": "BR5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR5",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074541667",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074541667",
+                "NAME1": "Little London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 568326,
+                "GEOMETRY_Y": 235146,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 568103,
+                "MBR_YMIN": 234793,
+                "MBR_XMAX": 568603,
+                "MBR_YMAX": 235293,
+                "POSTCODE_DISTRICT": "CM7",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/CM7",
+                "DISTRICT_BOROUGH": "Braintree",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000019795",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Essex",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000019687",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074543489",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074543489",
+                "NAME1": "Little London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 304475,
+                "GEOMETRY_Y": 289249,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 304273,
+                "MBR_YMIN": 289042,
+                "MBR_XMAX": 304773,
+                "MBR_YMAX": 289542,
+                "POSTCODE_DISTRICT": "SY17",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SY17",
+                "COUNTY_UNITARY": "Powys - Powys",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000025491",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/UnitaryAuthority",
+                "REGION": "Wales",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041424",
+                "COUNTRY": "Wales",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/wales"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074570193",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074570193",
+                "NAME1": "Little London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 547673,
+                "GEOMETRY_Y": 229492,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 547378,
+                "MBR_YMIN": 229201,
+                "MBR_XMAX": 547878,
+                "MBR_YMAX": 229701,
+                "POSTCODE_DISTRICT": "CM23",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/CM23",
+                "DISTRICT_BOROUGH": "Uttlesford",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000020033",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Essex",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000019687",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074573567",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074573567",
+                "NAME1": "Little London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 618547,
+                "GEOMETRY_Y": 323907,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 618282,
+                "MBR_YMIN": 323706,
+                "MBR_XMAX": 618782,
+                "MBR_YMAX": 324206,
+                "POSTCODE_DISTRICT": "NR10",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/NR10",
+                "DISTRICT_BOROUGH": "Broadland",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000006553",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Norfolk",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000007238",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "Eastern",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041425",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074573813",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074573813",
+                "NAME1": "Little London",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 506577,
+                "GEOMETRY_Y": 146740,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 506255,
+                "MBR_YMIN": 146505,
+                "MBR_XMAX": 506755,
+                "MBR_YMAX": 147047,
+                "POSTCODE_DISTRICT": "GU5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/GU5",
+                "DISTRICT_BOROUGH": "Guildford",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000014002",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Surrey",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000013965",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "South East",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041421",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074543687",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074543687",
+                "NAME1": "London Beach",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 588416,
+                "GEOMETRY_Y": 136145,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 588103,
+                "MBR_YMIN": 136037,
+                "MBR_XMAX": 588603,
+                "MBR_YMAX": 136741,
+                "POSTCODE_DISTRICT": "TN30",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TN30",
+                "DISTRICT_BOROUGH": "Ashford",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000018232",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/District",
+                "COUNTY_UNITARY": "Kent",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000018210",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/County",
+                "REGION": "South East",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041421",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074549608",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549608",
+                "NAME1": "Nash",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Hamlet",
+                "GEOMETRY_X": 540458,
+                "GEOMETRY_Y": 163928,
+                "MOST_DETAIL_VIEW_RES": 5000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 540112,
+                "MBR_YMIN": 163546,
+                "MBR_XMAX": 540612,
+                "MBR_YMAX": 164046,
+                "POSTCODE_DISTRICT": "BR2",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR2",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074561498",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074561498",
+                "NAME1": "Acton",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 520280,
+                "GEOMETRY_Y": 180066,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 518934,
+                "MBR_YMIN": 178644,
+                "MBR_XMAX": 522035,
+                "MBR_YMAX": 183249,
+                "POSTCODE_DISTRICT": "W3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/W3",
+                "DISTRICT_BOROUGH": "Ealing",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011399",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2657697",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Acton,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343911",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343911",
+                "NAME1": "Barnet",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 526618,
+                "GEOMETRY_Y": 196126,
+                "MOST_DETAIL_VIEW_RES": 39000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 522555,
+                "MBR_YMIN": 193687,
+                "MBR_XMAX": 528620,
+                "MBR_YMAX": 197754,
+                "POSTCODE_DISTRICT": "EN4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/EN4",
+                "DISTRICT_BOROUGH": "Barnet",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011378",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074559558",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559558",
+                "NAME1": "Battersea",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 527105,
+                "GEOMETRY_Y": 176161,
+                "MOST_DETAIL_VIEW_RES": 26000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 526180,
+                "MBR_YMIN": 173755,
+                "MBR_XMAX": 528729,
+                "MBR_YMAX": 177796,
+                "POSTCODE_DISTRICT": "SW11",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW11",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/6690602",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Battersea"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074551459",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074551459",
+                "NAME1": "Beckenham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 537449,
+                "GEOMETRY_Y": 169604,
+                "MOST_DETAIL_VIEW_RES": 34000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 534360,
+                "MBR_YMIN": 166461,
+                "MBR_XMAX": 539569,
+                "MBR_YMAX": 171476,
+                "POSTCODE_DISTRICT": "BR3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR3",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2656065",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Beckenham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074559226",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559226",
+                "NAME1": "Bermondsey",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 533627,
+                "GEOMETRY_Y": 179093,
+                "MOST_DETAIL_VIEW_RES": 15000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 532687,
+                "MBR_YMIN": 178167,
+                "MBR_XMAX": 534952,
+                "MBR_YMAX": 180499,
+                "POSTCODE_DISTRICT": "SE1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE1",
+                "DISTRICT_BOROUGH": "Southwark",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011013",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2655853",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Bermondsey"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074578541",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074578541",
+                "NAME1": "Bexley",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 549553,
+                "GEOMETRY_Y": 173602,
+                "MOST_DETAIL_VIEW_RES": 18000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 547240,
+                "MBR_YMIN": 172765,
+                "MBR_XMAX": 549960,
+                "MBR_YMAX": 174400,
+                "POSTCODE_DISTRICT": "DA5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/DA5",
+                "DISTRICT_BOROUGH": "Bexley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010759",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2655775",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Bexley"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074561689",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074561689",
+                "NAME1": "Brentford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 517697,
+                "GEOMETRY_Y": 177417,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 516017,
+                "MBR_YMIN": 177011,
+                "MBR_XMAX": 520292,
+                "MBR_YMAX": 179637,
+                "POSTCODE_DISTRICT": "TW8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TW8",
+                "DISTRICT_BOROUGH": "Hounslow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011489",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2654787",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Brentford"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343899",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343899",
+                "NAME1": "Bromley",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 540261,
+                "GEOMETRY_Y": 169118,
+                "MOST_DETAIL_VIEW_RES": 20000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 538629,
+                "MBR_YMIN": 167477,
+                "MBR_XMAX": 541185,
+                "MBR_YMAX": 170564,
+                "POSTCODE_DISTRICT": "BR1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR1",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074558888",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074558888",
+                "NAME1": "Camberwell",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 532476,
+                "GEOMETRY_Y": 176980,
+                "MOST_DETAIL_VIEW_RES": 21000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 531872,
+                "MBR_YMIN": 175101,
+                "MBR_XMAX": 533829,
+                "MBR_YMAX": 178316,
+                "POSTCODE_DISTRICT": "SE5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE5",
+                "DISTRICT_BOROUGH": "Southwark",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011013",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/3345438",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Camberwell"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074575971",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575971",
+                "NAME1": "Carshalton",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 528188,
+                "GEOMETRY_Y": 164544,
+                "MOST_DETAIL_VIEW_RES": 18000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 526585,
+                "MBR_YMIN": 163592,
+                "MBR_XMAX": 528588,
+                "MBR_YMAX": 166431,
+                "POSTCODE_DISTRICT": "SM5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SM5",
+                "DISTRICT_BOROUGH": "Sutton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010873",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2653646",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Carshalton"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074551841",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074551841",
+                "NAME1": "Catford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 537693,
+                "GEOMETRY_Y": 173590,
+                "MOST_DETAIL_VIEW_RES": 24000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 536510,
+                "MBR_YMIN": 171977,
+                "MBR_XMAX": 540189,
+                "MBR_YMAX": 174275,
+                "POSTCODE_DISTRICT": "SE6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE6",
+                "DISTRICT_BOROUGH": "Lewisham",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011039",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2653516",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Catford"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074550344",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074550344",
+                "NAME1": "Chislehurst",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 543847,
+                "GEOMETRY_Y": 170855,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 542569,
+                "MBR_YMIN": 167580,
+                "MBR_XMAX": 545633,
+                "MBR_YMAX": 171844,
+                "POSTCODE_DISTRICT": "BR7",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR7",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2653123",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Chislehurst"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074561090",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074561090",
+                "NAME1": "Chiswick",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 520700,
+                "GEOMETRY_Y": 178485,
+                "MOST_DETAIL_VIEW_RES": 18000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 520259,
+                "MBR_YMIN": 176083,
+                "MBR_XMAX": 522046,
+                "MBR_YMAX": 178809,
+                "POSTCODE_DISTRICT": "W4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/W4",
+                "DISTRICT_BOROUGH": "Hounslow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011489",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2653121",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Chiswick"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074559228",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559228",
+                "NAME1": "Clapham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 529697,
+                "GEOMETRY_Y": 175442,
+                "MOST_DETAIL_VIEW_RES": 12000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 528519,
+                "MBR_YMIN": 175023,
+                "MBR_XMAX": 530204,
+                "MBR_YMAX": 176851,
+                "POSTCODE_DISTRICT": "SW4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW4",
+                "DISTRICT_BOROUGH": "Lambeth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011144",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2652951",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Clapham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074319395",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074319395",
+                "NAME1": "Coldharbour",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 552155,
+                "GEOMETRY_Y": 179050,
+                "MOST_DETAIL_VIEW_RES": 15000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 551806,
+                "MBR_YMIN": 178698,
+                "MBR_XMAX": 552595,
+                "MBR_YMAX": 179198,
+                "POSTCODE_DISTRICT": "RM13",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/RM13",
+                "DISTRICT_BOROUGH": "Havering",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010807",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Coldharbour,_Havering"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074568222",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074568222",
+                "NAME1": "Coulsdon",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 529832,
+                "GEOMETRY_Y": 159625,
+                "MOST_DETAIL_VIEW_RES": 32000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 528039,
+                "MBR_YMIN": 156590,
+                "MBR_XMAX": 532962,
+                "MBR_YMAX": 160883,
+                "POSTCODE_DISTRICT": "CR5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/CR5",
+                "DISTRICT_BOROUGH": "Croydon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010896",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2652249",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Coulsdon"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074568828",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074568828",
+                "NAME1": "Crayford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 551362,
+                "GEOMETRY_Y": 174898,
+                "MOST_DETAIL_VIEW_RES": 23000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 549655,
+                "MBR_YMIN": 173735,
+                "MBR_XMAX": 553215,
+                "MBR_YMAX": 176331,
+                "POSTCODE_DISTRICT": "DA1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/DA1",
+                "DISTRICT_BOROUGH": "Bexley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010759",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2652046",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Crayford_Manor_House_Astronomical_Society"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074575690",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575690",
+                "NAME1": "Croydon",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 532327,
+                "GEOMETRY_Y": 165555,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 531594,
+                "MBR_YMIN": 162408,
+                "MBR_XMAX": 535418,
+                "MBR_YMAX": 167048,
+                "POSTCODE_DISTRICT": "CR9",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/CR9",
+                "DISTRICT_BOROUGH": "Croydon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010896",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2651817",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Croydon"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074544948",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074544948",
+                "NAME1": "Denham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 523096,
+                "GEOMETRY_Y": 193921,
+                "MOST_DETAIL_VIEW_RES": 15000,
+                "LEAST_DETAIL_VIEW_RES": 25000,
+                "MBR_XMIN": 522828,
+                "MBR_YMIN": 193652,
+                "MBR_XMAX": 523328,
+                "MBR_YMAX": 194152,
+                "POSTCODE_DISTRICT": "N20",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N20",
+                "DISTRICT_BOROUGH": "Barnet",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011378",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074558536",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074558536",
+                "NAME1": "Deptford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 537184,
+                "GEOMETRY_Y": 177255,
+                "MOST_DETAIL_VIEW_RES": 20000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 535344,
+                "MBR_YMIN": 175921,
+                "MBR_XMAX": 537871,
+                "MBR_YMAX": 179015,
+                "POSTCODE_DISTRICT": "SE8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE8",
+                "DISTRICT_BOROUGH": "Lewisham",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011039",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2651349",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Deptford"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343892",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343892",
+                "NAME1": "Ealing",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 517868,
+                "GEOMETRY_Y": 180795,
+                "MOST_DETAIL_VIEW_RES": 33000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 515654,
+                "MBR_YMIN": 178370,
+                "MBR_XMAX": 519436,
+                "MBR_YMAX": 183420,
+                "POSTCODE_DISTRICT": "W5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/W5",
+                "DISTRICT_BOROUGH": "Ealing",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011399",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343910",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343910",
+                "NAME1": "Edgware",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 519576,
+                "GEOMETRY_Y": 192258,
+                "MOST_DETAIL_VIEW_RES": 22000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 517960,
+                "MBR_YMIN": 191431,
+                "MBR_XMAX": 521413,
+                "MBR_YMAX": 194020,
+                "POSTCODE_DISTRICT": "HA8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA8",
+                "DISTRICT_BOROUGH": "Barnet",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011378",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343646",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343646",
+                "NAME1": "Edmonton",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 534344,
+                "GEOMETRY_Y": 193523,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 531844,
+                "MBR_YMIN": 191461,
+                "MBR_XMAX": 536408,
+                "MBR_YMAX": 195641,
+                "POSTCODE_DISTRICT": "N9",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N9",
+                "DISTRICT_BOROUGH": "Enfield",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011329",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2650209",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Edmonton,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074551840",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074551840",
+                "NAME1": "Eltham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 543047,
+                "GEOMETRY_Y": 174425,
+                "MOST_DETAIL_VIEW_RES": 32000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 540270,
+                "MBR_YMIN": 171432,
+                "MBR_XMAX": 545189,
+                "MBR_YMAX": 176334,
+                "POSTCODE_DISTRICT": "SE9",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE9",
+                "DISTRICT_BOROUGH": "Greenwich",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010777",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2650042",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Eltham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343645",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343645",
+                "NAME1": "Enfield",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 532620,
+                "GEOMETRY_Y": 196563,
+                "MOST_DETAIL_VIEW_RES": 43000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 530532,
+                "MBR_YMIN": 195470,
+                "MBR_XMAX": 537168,
+                "MBR_YMAX": 199940,
+                "POSTCODE_DISTRICT": "EN2",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/EN2",
+                "DISTRICT_BOROUGH": "Enfield",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011329",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074569176",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074569176",
+                "NAME1": "Erith",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 551421,
+                "GEOMETRY_Y": 178014,
+                "MOST_DETAIL_VIEW_RES": 24000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 548915,
+                "MBR_YMIN": 176838,
+                "MBR_XMAX": 552664,
+                "MBR_YMAX": 180003,
+                "POSTCODE_DISTRICT": "DA8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/DA8",
+                "DISTRICT_BOROUGH": "Bexley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010759",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2649937",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Erith"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074549619",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549619",
+                "NAME1": "Feltham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 510632,
+                "GEOMETRY_Y": 173185,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 508526,
+                "MBR_YMIN": 171139,
+                "MBR_XMAX": 512005,
+                "MBR_YMAX": 175516,
+                "POSTCODE_DISTRICT": "TW13",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TW13",
+                "DISTRICT_BOROUGH": "Hounslow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011489",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2649571",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Feltham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074576629",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074576629",
+                "NAME1": "Finchley",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 525120,
+                "GEOMETRY_Y": 190603,
+                "MOST_DETAIL_VIEW_RES": 39000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 523961,
+                "MBR_YMIN": 187182,
+                "MBR_XMAX": 527869,
+                "MBR_YMAX": 193189,
+                "POSTCODE_DISTRICT": "N3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N3",
+                "DISTRICT_BOROUGH": "Barnet",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011378",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2649441",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Finchley"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074563040",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074563040",
+                "NAME1": "Greenford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 514396,
+                "GEOMETRY_Y": 182238,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 513320,
+                "MBR_YMIN": 181077,
+                "MBR_XMAX": 516640,
+                "MBR_YMAX": 185655,
+                "POSTCODE_DISTRICT": "UB6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB6",
+                "DISTRICT_BOROUGH": "Ealing",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011399",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647972",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Greenford"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074578783",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074578783",
+                "NAME1": "Greenwich",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 538233,
+                "GEOMETRY_Y": 177402,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 537411,
+                "MBR_YMIN": 176207,
+                "MBR_XMAX": 540362,
+                "MBR_YMAX": 180547,
+                "POSTCODE_DISTRICT": "SE10",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE10",
+                "DISTRICT_BOROUGH": "Greenwich",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010777",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647937",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Greenwich"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074559563",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559563",
+                "NAME1": "Hackney",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 534945,
+                "GEOMETRY_Y": 184665,
+                "MOST_DETAIL_VIEW_RES": 22000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 534277,
+                "MBR_YMIN": 183208,
+                "MBR_XMAX": 537651,
+                "MBR_YMAX": 185500,
+                "POSTCODE_DISTRICT": "E8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/E8",
+                "DISTRICT_BOROUGH": "Hackney",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011199",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647694",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hackney_Central"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074541613",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074541613",
+                "NAME1": "Hampstead",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 526369,
+                "GEOMETRY_Y": 185770,
+                "MOST_DETAIL_VIEW_RES": 42000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 523942,
+                "MBR_YMIN": 183293,
+                "MBR_XMAX": 528194,
+                "MBR_YMAX": 189735,
+                "POSTCODE_DISTRICT": "NW3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/NW3",
+                "DISTRICT_BOROUGH": "Camden",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011244",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647553",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hampstead"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074552296",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074552296",
+                "NAME1": "Harrow",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 515179,
+                "GEOMETRY_Y": 187073,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 512092,
+                "MBR_YMIN": 185031,
+                "MBR_XMAX": 516724,
+                "MBR_YMAX": 189636,
+                "POSTCODE_DISTRICT": "HA1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA1",
+                "DISTRICT_BOROUGH": "Harrow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011391",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647425",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Harrow,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074564185",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074564185",
+                "NAME1": "Hayes",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 509843,
+                "GEOMETRY_Y": 179876,
+                "MOST_DETAIL_VIEW_RES": 34000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 507650,
+                "MBR_YMIN": 178316,
+                "MBR_XMAX": 512301,
+                "MBR_YMAX": 183498,
+                "POSTCODE_DISTRICT": "UB3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB3",
+                "DISTRICT_BOROUGH": "Hillingdon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011539",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647261",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hayes,_Hillingdon"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074551186",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074551186",
+                "NAME1": "Hendon",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 522830,
+                "GEOMETRY_Y": 188571,
+                "MOST_DETAIL_VIEW_RES": 29000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 521684,
+                "MBR_YMIN": 186394,
+                "MBR_XMAX": 524387,
+                "MBR_YMAX": 190804,
+                "POSTCODE_DISTRICT": "NW4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/NW4",
+                "DISTRICT_BOROUGH": "Barnet",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011378",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2647116",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hendon"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343947",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343947",
+                "NAME1": "Hillingdon",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 507636,
+                "GEOMETRY_Y": 184701,
+                "MOST_DETAIL_VIEW_RES": 26000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 506253,
+                "MBR_YMIN": 180930,
+                "MBR_XMAX": 508669,
+                "MBR_YMAX": 184945,
+                "POSTCODE_DISTRICT": "UB10",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB10",
+                "DISTRICT_BOROUGH": "Hillingdon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011539",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074574005",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074574005",
+                "NAME1": "Hornchurch",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 554024,
+                "GEOMETRY_Y": 187115,
+                "MOST_DETAIL_VIEW_RES": 27000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 551390,
+                "MBR_YMIN": 184835,
+                "MBR_XMAX": 555511,
+                "MBR_YMAX": 188205,
+                "POSTCODE_DISTRICT": "RM11",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/RM11",
+                "DISTRICT_BOROUGH": "Havering",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010807",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/6690863",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hornchurch"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074574948",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074574948",
+                "NAME1": "Hornsey",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 530455,
+                "GEOMETRY_Y": 189324,
+                "MOST_DETAIL_VIEW_RES": 14000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 529382,
+                "MBR_YMIN": 188499,
+                "MBR_XMAX": 531480,
+                "MBR_YMAX": 189864,
+                "POSTCODE_DISTRICT": "N8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N8",
+                "DISTRICT_BOROUGH": "Haringey",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011290",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2646580",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hornsey"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074561692",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074561692",
+                "NAME1": "Hounslow",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 513893,
+                "GEOMETRY_Y": 175576,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 510630,
+                "MBR_YMIN": 173476,
+                "MBR_XMAX": 515250,
+                "MBR_YMAX": 176786,
+                "POSTCODE_DISTRICT": "TW3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TW3",
+                "DISTRICT_BOROUGH": "Hounslow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011489",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2646517",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Hounslow"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074579136",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074579136",
+                "NAME1": "Ilford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 543838,
+                "GEOMETRY_Y": 186340,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 541297,
+                "MBR_YMIN": 185409,
+                "MBR_XMAX": 545634,
+                "MBR_YMAX": 188208,
+                "POSTCODE_DISTRICT": "IG1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/IG1",
+                "DISTRICT_BOROUGH": "Redbridge",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010955",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2646277",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Ilford"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074561688",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074561688",
+                "NAME1": "Isleworth",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 516178,
+                "GEOMETRY_Y": 175781,
+                "MOST_DETAIL_VIEW_RES": 20000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 514713,
+                "MBR_YMIN": 174116,
+                "MBR_XMAX": 516939,
+                "MBR_YMAX": 177235,
+                "POSTCODE_DISTRICT": "TW7",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/TW7",
+                "DISTRICT_BOROUGH": "Hounslow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011489",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2646004",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Isleworth"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074559233",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074559233",
+                "NAME1": "Islington",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 531671,
+                "GEOMETRY_Y": 183777,
+                "MOST_DETAIL_VIEW_RES": 17000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 530016,
+                "MBR_YMIN": 182917,
+                "MBR_XMAX": 532588,
+                "MBR_YMAX": 185202,
+                "POSTCODE_DISTRICT": "N1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N1",
+                "DISTRICT_BOROUGH": "Islington",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011281",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2646003",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Islington"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074551926",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074551926",
+                "NAME1": "Kenton",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 516760,
+                "GEOMETRY_Y": 188419,
+                "MOST_DETAIL_VIEW_RES": 21000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 515901,
+                "MBR_YMIN": 187416,
+                "MBR_XMAX": 519146,
+                "MBR_YMAX": 190158,
+                "POSTCODE_DISTRICT": "HA3",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA3",
+                "DISTRICT_BOROUGH": "Brent",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011447",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2645788",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Kenton"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074578779",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074578779",
+                "NAME1": "Lewisham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 538098,
+                "GEOMETRY_Y": 174995,
+                "MOST_DETAIL_VIEW_RES": 14000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 537360,
+                "MBR_YMIN": 174154,
+                "MBR_XMAX": 539149,
+                "MBR_YMAX": 176323,
+                "POSTCODE_DISTRICT": "SE13",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE13",
+                "DISTRICT_BOROUGH": "Lewisham",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011039",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2644556",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Lewisham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074575394",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575394",
+                "NAME1": "Merton",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 526411,
+                "GEOMETRY_Y": 169885,
+                "MOST_DETAIL_VIEW_RES": 11000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 525130,
+                "MBR_YMIN": 168745,
+                "MBR_XMAX": 526769,
+                "MBR_YMAX": 170193,
+                "POSTCODE_DISTRICT": "SW19",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW19",
+                "DISTRICT_BOROUGH": "Merton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010995",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074575392",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575392",
+                "NAME1": "Mitcham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 527355,
+                "GEOMETRY_Y": 168640,
+                "MOST_DETAIL_VIEW_RES": 29000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 526118,
+                "MBR_YMIN": 166836,
+                "MBR_XMAX": 530598,
+                "MBR_YMAX": 170915,
+                "POSTCODE_DISTRICT": "CR4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/CR4",
+                "DISTRICT_BOROUGH": "Merton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010995",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2642414",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Mitcham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343979",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343979",
+                "NAME1": "Morden",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 525817,
+                "GEOMETRY_Y": 168499,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 523269,
+                "MBR_YMIN": 165940,
+                "MBR_XMAX": 527544,
+                "MBR_YMAX": 169002,
+                "POSTCODE_DISTRICT": "SM4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SM4",
+                "DISTRICT_BOROUGH": "Merton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010995",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074563041",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074563041",
+                "NAME1": "Northolt",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 513071,
+                "GEOMETRY_Y": 184287,
+                "MOST_DETAIL_VIEW_RES": 24000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 511198,
+                "MBR_YMIN": 182348,
+                "MBR_XMAX": 514844,
+                "MBR_YMAX": 185827,
+                "POSTCODE_DISTRICT": "UB5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB5",
+                "DISTRICT_BOROUGH": "Ealing",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011399",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2641290",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Northolt"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074541727",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074541727",
+                "NAME1": "Northwood",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 509249,
+                "GEOMETRY_Y": 191464,
+                "MOST_DETAIL_VIEW_RES": 21000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 507333,
+                "MBR_YMIN": 190307,
+                "MBR_XMAX": 510504,
+                "MBR_YMAX": 192389,
+                "POSTCODE_DISTRICT": "HA6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA6",
+                "DISTRICT_BOROUGH": "Hillingdon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011539",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2641216",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Northwood,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074549229",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074549229",
+                "NAME1": "Orpington",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 546204,
+                "GEOMETRY_Y": 166159,
+                "MOST_DETAIL_VIEW_RES": 42000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 544022,
+                "MBR_YMIN": 164211,
+                "MBR_XMAX": 548159,
+                "MBR_YMAX": 170604,
+                "POSTCODE_DISTRICT": "BR6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/BR6",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2640894",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Orpington"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074576542",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074576542",
+                "NAME1": "Penge",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 535429,
+                "GEOMETRY_Y": 170332,
+                "MOST_DETAIL_VIEW_RES": 16000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 533470,
+                "MBR_YMIN": 169374,
+                "MBR_XMAX": 535926,
+                "MBR_YMAX": 171459,
+                "POSTCODE_DISTRICT": "SE20",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE20",
+                "DISTRICT_BOROUGH": "Bromley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010772",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/6941038",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Penge"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074560966",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074560966",
+                "NAME1": "Pinner",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 512165,
+                "GEOMETRY_Y": 189570,
+                "MOST_DETAIL_VIEW_RES": 27000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 510581,
+                "MBR_YMIN": 187553,
+                "MBR_XMAX": 513449,
+                "MBR_YMAX": 191749,
+                "POSTCODE_DISTRICT": "HA5",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA5",
+                "DISTRICT_BOROUGH": "Harrow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011391",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2640275",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Pinner"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074567896",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074567896",
+                "NAME1": "Purley",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 531267,
+                "GEOMETRY_Y": 161623,
+                "MOST_DETAIL_VIEW_RES": 22000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 528988,
+                "MBR_YMIN": 160000,
+                "MBR_XMAX": 532394,
+                "MBR_YMAX": 162577,
+                "POSTCODE_DISTRICT": "CR8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/CR8",
+                "DISTRICT_BOROUGH": "Croydon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010896",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2639842",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Purley,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074562835",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074562835",
+                "NAME1": "Putney",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 523957,
+                "GEOMETRY_Y": 175039,
+                "MOST_DETAIL_VIEW_RES": 25000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 522538,
+                "MBR_YMIN": 172558,
+                "MBR_XMAX": 524467,
+                "MBR_YMAX": 176357,
+                "POSTCODE_DISTRICT": "SW15",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW15",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2639835",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Putney"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074569858",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074569858",
+                "NAME1": "Rainham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 552037,
+                "GEOMETRY_Y": 182247,
+                "MOST_DETAIL_VIEW_RES": 24000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 550509,
+                "MBR_YMIN": 180570,
+                "MBR_XMAX": 554151,
+                "MBR_YMAX": 183556,
+                "POSTCODE_DISTRICT": "RM13",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/RM13",
+                "DISTRICT_BOROUGH": "Havering",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010807",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2639690",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Rainham,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074569531",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074569531",
+                "NAME1": "Romford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 551490,
+                "GEOMETRY_Y": 189133,
+                "MOST_DETAIL_VIEW_RES": 33000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 548866,
+                "MBR_YMIN": 186414,
+                "MBR_XMAX": 553933,
+                "MBR_YMAX": 190964,
+                "POSTCODE_DISTRICT": "RM1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/RM1",
+                "DISTRICT_BOROUGH": "Havering",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010807",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2639192",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Romford"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074561171",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074561171",
+                "NAME1": "Ruislip",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 509092,
+                "GEOMETRY_Y": 187621,
+                "MOST_DETAIL_VIEW_RES": 48000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 507725,
+                "MBR_YMIN": 183907,
+                "MBR_XMAX": 512566,
+                "MBR_YMAX": 191288,
+                "POSTCODE_DISTRICT": "HA4",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA4",
+                "DISTRICT_BOROUGH": "Hillingdon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011539",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2638976",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Ruislip"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074550722",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074550722",
+                "NAME1": "Sidcup",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 546204,
+                "GEOMETRY_Y": 172826,
+                "MOST_DETAIL_VIEW_RES": 26000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 544527,
+                "MBR_YMIN": 170403,
+                "MBR_XMAX": 548484,
+                "MBR_YMAX": 174213,
+                "POSTCODE_DISTRICT": "DA15",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/DA15",
+                "DISTRICT_BOROUGH": "Bexley",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010759",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2637861",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Sidcup"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074562074",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074562074",
+                "NAME1": "Southall",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 512825,
+                "GEOMETRY_Y": 180394,
+                "MOST_DETAIL_VIEW_RES": 33000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 510521,
+                "MBR_YMIN": 178552,
+                "MBR_XMAX": 515617,
+                "MBR_YMAX": 182802,
+                "POSTCODE_DISTRICT": "UB1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB1",
+                "DISTRICT_BOROUGH": "Ealing",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011399",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2637490",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Southall"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074577371",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074577371",
+                "NAME1": "Southgate",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 529680,
+                "GEOMETRY_Y": 193962,
+                "MOST_DETAIL_VIEW_RES": 26000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 528124,
+                "MBR_YMIN": 191777,
+                "MBR_XMAX": 531086,
+                "MBR_YMAX": 195820,
+                "POSTCODE_DISTRICT": "N14",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N14",
+                "DISTRICT_BOROUGH": "Enfield",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011329",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Southgate,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343954",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343954",
+                "NAME1": "Southwark",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 532192,
+                "GEOMETRY_Y": 179919,
+                "MOST_DETAIL_VIEW_RES": 13000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 531188,
+                "MBR_YMIN": 178875,
+                "MBR_XMAX": 533116,
+                "MBR_YMAX": 180691,
+                "POSTCODE_DISTRICT": "SE1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE1",
+                "DISTRICT_BOROUGH": "Southwark",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011013",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074553072",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074553072",
+                "NAME1": "Stanmore",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 517015,
+                "GEOMETRY_Y": 192329,
+                "MOST_DETAIL_VIEW_RES": 35000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 514520,
+                "MBR_YMIN": 190003,
+                "MBR_XMAX": 519942,
+                "MBR_YMAX": 194396,
+                "POSTCODE_DISTRICT": "HA7",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA7",
+                "DISTRICT_BOROUGH": "Harrow",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011391",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2637063",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Stanmore"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074579133",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074579133",
+                "NAME1": "Stratford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 538840,
+                "GEOMETRY_Y": 184272,
+                "MOST_DETAIL_VIEW_RES": 18000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 537329,
+                "MBR_YMIN": 182798,
+                "MBR_XMAX": 539561,
+                "MBR_YMAX": 185588,
+                "POSTCODE_DISTRICT": "E15",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/E15",
+                "DISTRICT_BOROUGH": "Newham",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010929",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2636714",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Stratford,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074575698",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575698",
+                "NAME1": "Streatham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 530139,
+                "GEOMETRY_Y": 171999,
+                "MOST_DETAIL_VIEW_RES": 28000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 528804,
+                "MBR_YMIN": 169624,
+                "MBR_XMAX": 531437,
+                "MBR_YMAX": 173994,
+                "POSTCODE_DISTRICT": "SW16",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW16",
+                "DISTRICT_BOROUGH": "Lambeth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011144",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2636675",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Streatham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343897",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343897",
+                "NAME1": "Sutton",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 525864,
+                "GEOMETRY_Y": 164316,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 524772,
+                "MBR_YMIN": 162055,
+                "MBR_XMAX": 526782,
+                "MBR_YMAX": 166640,
+                "POSTCODE_DISTRICT": "SM1",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SM1",
+                "DISTRICT_BOROUGH": "Sutton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010873",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074574409",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074574409",
+                "NAME1": "Tottenham",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 533604,
+                "GEOMETRY_Y": 189510,
+                "MOST_DETAIL_VIEW_RES": 26000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 531480,
+                "MBR_YMIN": 187950,
+                "MBR_XMAX": 535529,
+                "MBR_YMAX": 191793,
+                "POSTCODE_DISTRICT": "N15",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/N15",
+                "DISTRICT_BOROUGH": "Haringey",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011290",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2635608",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Tottenham"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074573321",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074573321",
+                "NAME1": "Upminster",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 556014,
+                "GEOMETRY_Y": 186548,
+                "MOST_DETAIL_VIEW_RES": 22000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 554855,
+                "MBR_YMIN": 185391,
+                "MBR_XMAX": 557248,
+                "MBR_YMAX": 188816,
+                "POSTCODE_DISTRICT": "RM14",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/RM14",
+                "DISTRICT_BOROUGH": "Havering",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010807",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2635150",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Upminster"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074565718",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074565718",
+                "NAME1": "Uxbridge",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 505528,
+                "GEOMETRY_Y": 184129,
+                "MOST_DETAIL_VIEW_RES": 21000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 504454,
+                "MBR_YMIN": 182376,
+                "MBR_XMAX": 506893,
+                "MBR_YMAX": 185632,
+                "POSTCODE_DISTRICT": "UB8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/UB8",
+                "DISTRICT_BOROUGH": "Hillingdon",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011539",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2635042",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Uxbridge"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074575967",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074575967",
+                "NAME1": "Wallington",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 528955,
+                "GEOMETRY_Y": 163652,
+                "MOST_DETAIL_VIEW_RES": 20000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 528184,
+                "MBR_YMIN": 162582,
+                "MBR_XMAX": 529792,
+                "MBR_YMAX": 165683,
+                "POSTCODE_DISTRICT": "SM6",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SM6",
+                "DISTRICT_BOROUGH": "Sutton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010873",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2634867",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Wallington,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074576254",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074576254",
+                "NAME1": "Wandsworth",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 525661,
+                "GEOMETRY_Y": 174637,
+                "MOST_DETAIL_VIEW_RES": 20000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 524273,
+                "MBR_YMIN": 172891,
+                "MBR_XMAX": 527413,
+                "MBR_YMAX": 175697,
+                "POSTCODE_DISTRICT": "SW18",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW18",
+                "DISTRICT_BOROUGH": "Wandsworth",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011127",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2634812",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Wandsworth"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074552140",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074552140",
+                "NAME1": "Wanstead",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 540447,
+                "GEOMETRY_Y": 188594,
+                "MOST_DETAIL_VIEW_RES": 18000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 539909,
+                "MBR_YMIN": 187206,
+                "MBR_XMAX": 541420,
+                "MBR_YMAX": 189925,
+                "POSTCODE_DISTRICT": "E11",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/E11",
+                "DISTRICT_BOROUGH": "Redbridge",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010955",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2634803",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Wanstead"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074563042",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074563042",
+                "NAME1": "Wembley",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 518419,
+                "GEOMETRY_Y": 185248,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 516053,
+                "MBR_YMIN": 184329,
+                "MBR_XMAX": 520606,
+                "MBR_YMAX": 187949,
+                "POSTCODE_DISTRICT": "HA9",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/HA9",
+                "DISTRICT_BOROUGH": "Brent",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011447",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2634549",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Wembley"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074562838",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074562838",
+                "NAME1": "Willesden",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 522522,
+                "GEOMETRY_Y": 184646,
+                "MOST_DETAIL_VIEW_RES": 20000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 521403,
+                "MBR_YMIN": 183086,
+                "MBR_XMAX": 523646,
+                "MBR_YMAX": 186103,
+                "POSTCODE_DISTRICT": "NW10",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/NW10",
+                "DISTRICT_BOROUGH": "Brent",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000011447",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2633907",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Willesden"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074541243",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074541243",
+                "NAME1": "Wimbledon",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 524915,
+                "GEOMETRY_Y": 170559,
+                "MOST_DETAIL_VIEW_RES": 35000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 521440,
+                "MBR_YMIN": 169313,
+                "MBR_XMAX": 526763,
+                "MBR_YMAX": 174009,
+                "POSTCODE_DISTRICT": "SW19",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SW19",
+                "DISTRICT_BOROUGH": "Merton",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010995",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2633866",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Wimbledon,_London"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074343914",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074343914",
+                "NAME1": "Woodford",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 540921,
+                "GEOMETRY_Y": 191835,
+                "MOST_DETAIL_VIEW_RES": 30000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 539151,
+                "MBR_YMIN": 189582,
+                "MBR_XMAX": 543060,
+                "MBR_YMAX": 194179,
+                "POSTCODE_DISTRICT": "IG8",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/IG8",
+                "DISTRICT_BOROUGH": "Redbridge",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010955",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england"
+            }
+        },
+        {
+            "GAZETTEER_ENTRY": {
+                "ID": "osgb4000000074578663",
+                "NAMES_URI": "http://data.ordnancesurvey.co.uk/id/4000000074578663",
+                "NAME1": "Woolwich",
+                "TYPE": "populatedPlace",
+                "LOCAL_TYPE": "Other Settlement",
+                "GEOMETRY_X": 543354,
+                "GEOMETRY_Y": 178854,
+                "MOST_DETAIL_VIEW_RES": 21000,
+                "LEAST_DETAIL_VIEW_RES": 60000,
+                "MBR_XMIN": 541660,
+                "MBR_YMIN": 177231,
+                "MBR_XMAX": 544364,
+                "MBR_YMAX": 180531,
+                "POSTCODE_DISTRICT": "SE18",
+                "POSTCODE_DISTRICT_URI": "http://data.ordnancesurvey.co.uk/id/postcodedistrict/SE18",
+                "DISTRICT_BOROUGH": "Greenwich",
+                "DISTRICT_BOROUGH_URI": "http://data.ordnancesurvey.co.uk/id/7000000000010777",
+                "DISTRICT_BOROUGH_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/LondonBorough",
+                "COUNTY_UNITARY": "Greater London",
+                "COUNTY_UNITARY_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041441",
+                "COUNTY_UNITARY_TYPE": "http://data.ordnancesurvey.co.uk/ontology/admingeo/GreaterLondonAuthority",
+                "REGION": "London",
+                "REGION_URI": "http://data.ordnancesurvey.co.uk/id/7000000000041428",
+                "COUNTRY": "England",
+                "COUNTRY_URI": "http://data.ordnancesurvey.co.uk/id/country/england",
+                "SAME_AS_GEONAMES": "http://sws.geonames.org/2633583",
+                "SAME_AS_DBPEDIA": "http://dbpedia.org/resource/Woolwich"
+            }
+        }
+    ]
+}
diff --git a/test/fixtures/uk_ordnance_survey_names_no_results b/test/fixtures/uk_ordnance_survey_names_no_results
new file mode 100644
index 0000000..a91f413
--- /dev/null
+++ b/test/fixtures/uk_ordnance_survey_names_no_results
@@ -0,0 +1,11 @@
+{
+    "header": {
+        "uri": "https://api.os.uk/search/names/v1/find?query=sxz%60x%60zx%60xz%60xz%60&fq=local_type%3ACity%20local_type%3AHamlet%20local_type%3AOther_Settlement%20local_type%3ATown%20local_type%3AVillage%20local_type%3APostcode",
+        "query": "sxz`x`zx`xz`xz`",
+        "format": "JSON",
+        "maxresults": 100,
+        "offset": 0,
+        "totalresults": 0,
+        "filter": "fq=local_type:City local_type:Hamlet local_type:Other_Settlement local_type:Town local_type:Village local_type:Postcode"
+    }
+}
diff --git a/test/fixtures/yandex_black_sea b/test/fixtures/yandex_black_sea
new file mode 100644
index 0000000..9f7c736
--- /dev/null
+++ b/test/fixtures/yandex_black_sea
@@ -0,0 +1,74 @@
+{
+  "response": {
+    "GeoObjectCollection": {
+      "metaDataProperty": {
+        "GeocoderResponseMetaData": {
+          "request": "32,43",
+          "found": "2",
+          "results": "10",
+          "Point": {
+            "pos": "32.000000 43.000000"
+          }
+        }
+      },
+      "featureMember": [
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "hydro",
+                "text": "Black Sea",
+                "precision": "other",
+                "AddressDetails": {
+                  "Locality": {
+                    "Premise": {
+                      "PremiseName": "Black Sea"
+                    }
+                  }
+                }
+              }
+            },
+            "name": "Black Sea",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "27.442409 40.908662",
+                "upperCorner": "41.777787 46.627275"
+              }
+            },
+            "Point": {
+              "pos": "34.188281 43.229215"
+            }
+          }
+        },
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "hydro",
+                "text": "Black Sea",
+                "precision": "other",
+                "AddressDetails": {
+                  "Locality": {
+                    "Premise": {
+                      "PremiseName": "Black Sea"
+                    }
+                  }
+                }
+              }
+            },
+            "name": "Black Sea",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "27.628351 40.908893",
+                "upperCorner": "41.777796 46.627999"
+              }
+            },
+            "Point": {
+              "pos": "33.383633 44.235582"
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/test/fixtures/yandex_canada_rue_dupuis_14 b/test/fixtures/yandex_canada_rue_dupuis_14
new file mode 100644
index 0000000..3fe85cf
--- /dev/null
+++ b/test/fixtures/yandex_canada_rue_dupuis_14
@@ -0,0 +1,446 @@
+{
+    "response":{
+        "Attribution":"",
+            "GeoObjectCollection":{
+                "metaDataProperty":{
+                    "GeocoderResponseMetaData":{
+                        "request":"canada rue dupuis 14",
+                        "found":"52",
+                        "results":"10"
+                    }
+                },
+                "featureMember":[
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"house",
+                                "text":"Canada, Quebec, Beauharnois-Salaberry, Beauharnois, Rue Dupuis, 14",
+                                "precision":"exact",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Beauharnois-Salaberry, Beauharnois, Rue Dupuis, 14",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Beauharnois-Salaberry",
+                                                "Locality":{
+                                                    "LocalityName":"Beauharnois",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis",
+                                                        "Premise":{
+                                                            "PremiseNumber":"14"
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Beauharnois, Beauharnois-Salaberry, Quebec, Canada",
+                        "name":"Rue Dupuis, 14",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.880377 45.306461",
+                                "upperCorner":"-73.876281 45.309351"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.878329 45.307906"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"house",
+                                "text":"Canada, Quebec, Roussillon, St-Philippe, Rue Dupuis, 14",
+                                "precision":"exact",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Roussillon, St-Philippe, Rue Dupuis, 14",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Roussillon",
+                                                "Locality":{
+                                                    "LocalityName":"St-Philippe",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis",
+                                                        "Premise":{
+                                                            "PremiseNumber":"14"
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"St-Philippe, Roussillon, Quebec, Canada",
+                        "name":"Rue Dupuis, 14",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.491263 45.371702",
+                                "upperCorner":"-73.487167 45.374589"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.489215 45.373146"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"house",
+                                "text":"Canada, Quebec, Témiscamingue, Notre-Dame-du-Nord, Rue Dupuis, 14",
+                                "precision":"exact",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Témiscamingue, Notre-Dame-du-Nord, Rue Dupuis, 14",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Témiscamingue",
+                                                "Locality":{
+                                                    "LocalityName":"Notre-Dame-du-Nord",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis",
+                                                        "Premise":{
+                                                            "PremiseNumber":"14"
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Notre-Dame-du-Nord, Témiscamingue, Quebec, Canada",
+                        "name":"Rue Dupuis, 14",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-79.484724 47.596109",
+                                "upperCorner":"-79.480628 47.598879"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-79.482676 47.597494"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"house",
+                                "text":"Canada, Quebec, Montcalm, St-Jacques, Rue Dupuis, 14",
+                                "precision":"exact",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Montcalm, St-Jacques, Rue Dupuis, 14",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Montcalm",
+                                                "Locality":{
+                                                    "LocalityName":"St-Jacques",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis",
+                                                        "Premise":{
+                                                            "PremiseNumber":"14"
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"St-Jacques, Montcalm, Quebec, Canada",
+                        "name":"Rue Dupuis, 14",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.573136 45.945209",
+                                "upperCorner":"-73.569039 45.948066"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.571088 45.946637"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"street",
+                                "text":"Canada, Quebec, Le Haut-Richelieu, St-Jean-sur-Richelieu, St-Luc, Rue Dupuis",
+                                "precision":"street",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Le Haut-Richelieu, St-Jean-sur-Richelieu, St-Luc, Rue Dupuis",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Le Haut-Richelieu",
+                                                "Locality":{
+                                                    "LocalityName":"St-Luc",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis"
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"St-Luc, St-Jean-sur-Richelieu, Le Haut-Richelieu, Quebec, Canada",
+                        "name":"Rue Dupuis",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.253767 45.371677",
+                                "upperCorner":"-73.249814 45.383244"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.250829 45.377875"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"house",
+                                "text":"Canada, Quebec, Thérèse-de-Blainville, Blainville, Rue Corinne-Dupuis, 14",
+                                "precision":"exact",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Thérèse-de-Blainville, Blainville, Rue Corinne-Dupuis, 14",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Thérèse-de-Blainville",
+                                                "Locality":{
+                                                    "LocalityName":"Blainville",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Corinne-Dupuis",
+                                                        "Premise":{
+                                                            "PremiseNumber":"14"
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Blainville, Thérèse-de-Blainville, Quebec, Canada",
+                        "name":"Rue Corinne-Dupuis, 14",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.907956 45.692721",
+                                "upperCorner":"-73.903859 45.695592"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.905908 45.694157"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"house",
+                                "text":"Canada, Quebec, Gatineau, Hull, Rue Hormidas-Dupuis, 14",
+                                "precision":"exact",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Gatineau, Hull, Rue Hormidas-Dupuis, 14",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Gatineau",
+                                                "Locality":{
+                                                    "DependentLocality":{
+                                                        "DependentLocalityName":"Hull",
+                                                        "Thoroughfare":{
+                                                            "ThoroughfareName":"Rue Hormidas-Dupuis",
+                                                            "Premise":{
+                                                                "PremiseNumber":"14"
+                                                            }
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Hull, Gatineau, Quebec, Canada",
+                        "name":"Rue Hormidas-Dupuis, 14",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-75.745594 45.421953",
+                                "upperCorner":"-75.741498 45.424838"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-75.743546 45.423396"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"street",
+                                "text":"Canada, Quebec, Roussillon, Châteauguay, Rue Dupuis",
+                                "precision":"street",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Roussillon, Châteauguay, Rue Dupuis",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Roussillon",
+                                                "Locality":{
+                                                    "LocalityName":"Châteauguay",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis"
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Châteauguay, Roussillon, Quebec, Canada",
+                        "name":"Rue Dupuis",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.708691 45.344539",
+                                "upperCorner":"-73.703499 45.345287"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.706086 45.344983"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"street",
+                                "text":"Canada, Quebec, Longueuil, Rue Dupuis",
+                                "precision":"street",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, Longueuil, Rue Dupuis",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"Longueuil",
+                                                "Locality":{
+                                                    "LocalityName":"Longueuil",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis"
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Longueuil, Quebec, Canada",
+                        "name":"Rue Dupuis",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-73.463631 45.515456",
+                                "upperCorner":"-73.459193 45.516568"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-73.461385 45.516094"
+                        }
+                    }
+                },
+                {
+                    "GeoObject":{
+                        "metaDataProperty":{
+                            "GeocoderMetaData":{
+                                "kind":"street",
+                                "text":"Canada, Quebec, La Vallée-de-l'Or, Val-d'Or, Rue Dupuis",
+                                "precision":"street",
+                                "AddressDetails":{
+                                    "Country":{
+                                        "AddressLine":"Quebec, La Vallée-de-l'Or, Val-d'Or, Rue Dupuis",
+                                        "CountryNameCode":"CA",
+                                        "CountryName":"Canada",
+                                        "AdministrativeArea":{
+                                            "AdministrativeAreaName":"Quebec",
+                                            "SubAdministrativeArea":{
+                                                "SubAdministrativeAreaName":"La Vallée-de-l'Or",
+                                                "Locality":{
+                                                    "LocalityName":"Val-d'Or",
+                                                    "Thoroughfare":{
+                                                        "ThoroughfareName":"Rue Dupuis"
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        },
+                        "description":"Val-d'Or, La Vallée-de-l'Or, Quebec, Canada",
+                        "name":"Rue Dupuis",
+                        "boundedBy":{
+                            "Envelope":{
+                                "lowerCorner":"-77.810408 48.093635",
+                                "upperCorner":"-77.807839 48.097637"
+                            }
+                        },
+                        "Point":{
+                            "pos":"-77.810390 48.095663"
+                        }
+                    }
+                }
+                ]
+            }
+    }
+}
diff --git a/test/fixtures/yandex_invalid_key b/test/fixtures/yandex_invalid_key
new file mode 100644
index 0000000..52dcabe
--- /dev/null
+++ b/test/fixtures/yandex_invalid_key
@@ -0,0 +1 @@
+{"statusCode":403,"error":"Forbidden","message":"Invalid key"}
diff --git a/test/fixtures/yandex_kremlin b/test/fixtures/yandex_kremlin
new file mode 100644
index 0000000..77ac194
--- /dev/null
+++ b/test/fixtures/yandex_kremlin
@@ -0,0 +1,79 @@
+{
+  "response": {
+    "GeoObjectCollection": {
+      "metaDataProperty": {
+        "GeocoderResponseMetaData": {
+          "request": "Kremlin, Moscow, Russia",
+          "found": "8",
+          "results": "10"
+        }
+      },
+      "featureMember": [
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "district",
+                "text": "Russia, Moscow, Moscow Kremlin",
+                "precision": "other",
+                "Address": {
+                  "country_code": "RU",
+                  "formatted": "Moscow, Moscow Kremlin",
+                  "Components": [
+                    {
+                      "kind": "country",
+                      "name": "Russia"
+                    },
+                    {
+                      "kind": "province",
+                      "name": "Tsentralny federalny okrug"
+                    },
+                    {
+                      "kind": "province",
+                      "name": "Moscow"
+                    },
+                    {
+                      "kind": "locality",
+                      "name": "Moscow"
+                    },
+                    {
+                      "kind": "district",
+                      "name": "Moscow Kremlin"
+                    }
+                  ]
+                },
+                "AddressDetails": {
+                  "Country": {
+                    "AddressLine": "Moscow, Moscow Kremlin",
+                    "CountryNameCode": "RU",
+                    "CountryName": "Russia",
+                    "AdministrativeArea": {
+                      "AdministrativeAreaName": "Moscow",
+                      "Locality": {
+                        "LocalityName": "Moscow",
+                        "DependentLocality": {
+                          "DependentLocalityName": "Moscow Kremlin"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "description": "Moscow, Russia",
+            "name": "Moscow Kremlin",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "37.612587 55.748189",
+                "upperCorner": "37.623187 55.755044"
+              }
+            },
+            "Point": {
+              "pos": "37.617734 55.751999"
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/test/fixtures/yandex_new_york b/test/fixtures/yandex_new_york
new file mode 100644
index 0000000..5bfa354
--- /dev/null
+++ b/test/fixtures/yandex_new_york
@@ -0,0 +1,48 @@
+{
+  "response": {
+    "Attribution": "",
+    "GeoObjectCollection": {
+      "metaDataProperty": {
+        "GeocoderResponseMetaData": {
+          "request": "New York, NY",
+          "found": "21",
+          "results": "10"
+        }
+      },
+      "featureMember": [
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "province",
+                "text": "United States, New York",
+                "precision": "other",
+                "AddressDetails": {
+                  "Country": {
+                    "AddressLine": "New York",
+                    "CountryNameCode": "US",
+                    "CountryName": "United States",
+                    "AdministrativeArea": {
+                      "AdministrativeAreaName": "New York"
+                    }
+                  }
+                }
+              }
+            },
+            "description": "United States",
+            "name": "New York",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "-79.762115 40.477414",
+                "upperCorner": "-71.668635 45.016078"
+              }
+            },
+            "Point": {
+              "pos": "-74.007112 40.714545"
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/test/fixtures/yandex_no_administrative_area b/test/fixtures/yandex_no_administrative_area
new file mode 100644
index 0000000..baa984a
--- /dev/null
+++ b/test/fixtures/yandex_no_administrative_area
@@ -0,0 +1,53 @@
+{
+   "response":{
+      "GeoObjectCollection":{
+         "metaDataProperty":{
+            "GeocoderResponseMetaData":{
+               "request":"13.813139,100.560291",
+               "found":"1",
+               "results":"1",
+               "Point":{
+                  "pos":"13.813139 100.560291"
+               }
+            }
+         },
+         "featureMember":[
+            {
+               "GeoObject":{
+                  "metaDataProperty":{
+                     "GeocoderMetaData":{
+                        "kind":"house",
+                        "text":"Thailand, Phahon Yothin Road, 1130/5",
+                        "precision":"exact",
+                        "AddressDetails":{
+                           "Country":{
+                              "AddressLine":"Phahon Yothin Road, 1130/5",
+                              "CountryNameCode":"TH",
+                              "CountryName":"Thailand",
+                              "Thoroughfare":{
+                                 "ThoroughfareName":"Phahon Yothin Road",
+                                 "Premise":{
+                                    "PremiseNumber":"1130/5"
+                                 }
+                              }
+                           }
+                        }
+                     }
+                  },
+                  "description":"Thailand",
+                  "name":"Phahon Yothin Road, 1130/5",
+                  "boundedBy":{
+                     "Envelope":{
+                        "lowerCorner":"100.552005 13.805102",
+                        "upperCorner":"100.568462 13.821184"
+                     }
+                  },
+                  "Point":{
+                     "pos":"100.560234 13.813143"
+                  }
+               }
+            }
+         ]
+      }
+   }
+}
diff --git a/test/fixtures/yandex_no_city_and_town b/test/fixtures/yandex_no_city_and_town
new file mode 100644
index 0000000..4493bf3
--- /dev/null
+++ b/test/fixtures/yandex_no_city_and_town
@@ -0,0 +1,112 @@
+{
+   "response":{
+      "GeoObjectCollection":{
+         "metaDataProperty":{
+            "GeocoderResponseMetaData":{
+               "request":"57.423359,55.892596",
+               "found":"3",
+               "results":"10",
+               "Point":{
+                  "pos":"57.423359 55.892596"
+               }
+            }
+         },
+         "featureMember":[
+            {
+               "GeoObject":{
+                  "metaDataProperty":{
+                     "GeocoderMetaData":{
+                        "kind":"area",
+                        "text":"Россия, республика Башкортостан, Караидельский район",
+                        "precision":"other",
+                        "AddressDetails":{
+                           "Country":{
+                              "AddressLine":"республика Башкортостан, Караидельский район",
+                              "CountryNameCode":"RU",
+                              "CountryName":"Россия",
+                              "AdministrativeArea":{
+                                 "AdministrativeAreaName":"республика Башкортостан",
+                                 "SubAdministrativeArea":{
+                                    "SubAdministrativeAreaName":"Караидельский район"
+                                 }
+                              }
+                           }
+                        }
+                     }
+                  },
+                  "description":"республика Башкортостан, Россия",
+                  "name":"Караидельский район",
+                  "boundedBy":{
+                     "Envelope":{
+                        "lowerCorner":"56.231384 55.462814",
+                        "upperCorner":"57.705348 56.076117"
+                     }
+                  },
+                  "Point":{
+                     "pos":"57.423359 55.892596"
+                  }
+               }
+            },
+            {
+               "GeoObject":{
+                  "metaDataProperty":{
+                     "GeocoderMetaData":{
+                        "kind":"province",
+                        "text":"Россия, республика Башкортостан",
+                        "precision":"other",
+                        "AddressDetails":{
+                           "Country":{
+                              "AddressLine":"республика Башкортостан",
+                              "CountryNameCode":"RU",
+                              "CountryName":"Россия",
+                              "AdministrativeArea":{
+                                 "AdministrativeAreaName":"республика Башкортостан"
+                              }
+                           }
+                        }
+                     }
+                  },
+                  "description":"Россия",
+                  "name":"республика Башкортостан",
+                  "boundedBy":{
+                     "Envelope":{
+                        "lowerCorner":"53.157475 51.571991",
+                        "upperCorner":"60.001577 56.533651"
+                     }
+                  },
+                  "Point":{
+                     "pos":"56.579526 54.127354"
+                  }
+               }
+            },
+            {
+               "GeoObject":{
+                  "metaDataProperty":{
+                     "GeocoderMetaData":{
+                        "kind":"country",
+                        "text":"Россия",
+                        "precision":"other",
+                        "AddressDetails":{
+                           "Country":{
+                              "CountryNameCode":"RU",
+                              "CountryName":"Россия"
+                           }
+                        }
+                     }
+                  },
+                  "name":"Россия",
+                  "boundedBy":{
+                     "Envelope":{
+                        "lowerCorner":"19.641673 36.84312",
+                        "upperCorner":"179.999997 81.848739"
+                     }
+                  },
+                  "Point":{
+                     "pos":"37.617761 55.755773"
+                  }
+               }
+            }
+         ]
+      }
+   }
+}
\ No newline at end of file
diff --git a/test/fixtures/yandex_no_results b/test/fixtures/yandex_no_results
new file mode 100644
index 0000000..6645611
--- /dev/null
+++ b/test/fixtures/yandex_no_results
@@ -0,0 +1,16 @@
+{
+    "response": {
+        "GeoObjectCollection": {
+            "metaDataProperty": {
+                "GeocoderResponseMetaData": {
+                    "request": "blah",
+                    "found": "0",
+                    "results": "10"
+                }
+            },
+            "featureMember": [
+
+            ]
+        }
+    }
+}
diff --git a/test/fixtures/yandex_ontario b/test/fixtures/yandex_ontario
new file mode 100644
index 0000000..478989c
--- /dev/null
+++ b/test/fixtures/yandex_ontario
@@ -0,0 +1,61 @@
+{
+  "response": {
+    "GeoObjectCollection": {
+      "metaDataProperty": {
+        "GeocoderResponseMetaData": {
+          "request": "ontario",
+          "found": "57",
+          "results": "10"
+        }
+      },
+      "featureMember": [
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "province",
+                "text": "Canada, Ontario",
+                "precision": "other",
+                "Address": {
+                  "country_code": "CA",
+                  "formatted": "Ontario",
+                  "Components": [
+                    {
+                      "kind": "country",
+                      "name": "Canada"
+                    },
+                    {
+                      "kind": "province",
+                      "name": "Ontario"
+                    }
+                  ]
+                },
+                "AddressDetails": {
+                  "Country": {
+                    "AddressLine": "Ontario",
+                    "CountryNameCode": "CA",
+                    "CountryName": "Canada",
+                    "AdministrativeArea": {
+                      "AdministrativeAreaName": "Ontario"
+                    }
+                  }
+                }
+              }
+            },
+            "description": "Canada",
+            "name": "Ontario",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "-95.153382 41.704494",
+                "upperCorner": "-74.321387 56.88699"
+              }
+            },
+            "Point": {
+              "pos": "-87.170557 49.294248"
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/test/fixtures/yandex_putilkovo_novotushinskaya_5 b/test/fixtures/yandex_putilkovo_novotushinskaya_5
new file mode 100644
index 0000000..e2626f0
--- /dev/null
+++ b/test/fixtures/yandex_putilkovo_novotushinskaya_5
@@ -0,0 +1,97 @@
+{
+  "response": {
+    "GeoObjectCollection": {
+      "metaDataProperty": {
+        "GeocoderResponseMetaData": {
+          "request": "putilkovo novotushinskaya 5",
+          "found": "1",
+          "results": "10"
+        }
+      },
+      "featureMember": [
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "house",
+                "text": "Russia, Moscow Region, gorodskoy okrug Krasnogorsk, derevnya Putilkovo, Novotushinskaya ulitsa, 5",
+                "precision": "exact",
+                "Address": {
+                  "country_code": "RU",
+                  "postal_code": "143441",
+                  "formatted": "Moscow Region, gorodskoy okrug Krasnogorsk, derevnya Putilkovo, Novotushinskaya ulitsa, 5",
+                  "Components": [
+                    {
+                      "kind": "country",
+                      "name": "Russia"
+                    },
+                    {
+                      "kind": "province",
+                      "name": "Tsentralny federalny okrug"
+                    },
+                    {
+                      "kind": "province",
+                      "name": "Moscow Region"
+                    },
+                    {
+                      "kind": "area",
+                      "name": "gorodskoy okrug Krasnogorsk"
+                    },
+                    {
+                      "kind": "locality",
+                      "name": "derevnya Putilkovo"
+                    },
+                    {
+                      "kind": "street",
+                      "name": "Novotushinskaya ulitsa"
+                    },
+                    {
+                      "kind": "house",
+                      "name": "5"
+                    }
+                  ]
+                },
+                "AddressDetails": {
+                  "Country": {
+                    "AddressLine": "Moscow Region, gorodskoy okrug Krasnogorsk, derevnya Putilkovo, Novotushinskaya ulitsa, 5",
+                    "CountryNameCode": "RU",
+                    "CountryName": "Russia",
+                    "AdministrativeArea": {
+                      "AdministrativeAreaName": "Moscow Region",
+                      "SubAdministrativeArea": {
+                        "SubAdministrativeAreaName": "gorodskoy okrug Krasnogorsk",
+                        "Locality": {
+                          "LocalityName": "derevnya Putilkovo",
+                          "Thoroughfare": {
+                            "ThoroughfareName": "Novotushinskaya ulitsa",
+                            "Premise": {
+                              "PremiseNumber": "5",
+                              "PostalCode": {
+                                "PostalCodeNumber": "143441"
+                              }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "description": "derevnya Putilkovo, gorodskoy okrug Krasnogorsk, Moscow Region, Russia",
+            "name": "Novotushinskaya ulitsa, 5",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "37.399416 55.86995",
+                "upperCorner": "37.407627 55.874567"
+              }
+            },
+            "Point": {
+              "pos": "37.403522 55.872258"
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/test/fixtures/yandex_volga_river b/test/fixtures/yandex_volga_river
new file mode 100644
index 0000000..a314c19
--- /dev/null
+++ b/test/fixtures/yandex_volga_river
@@ -0,0 +1,63 @@
+{
+  "response": {
+    "GeoObjectCollection": {
+      "metaDataProperty": {
+        "GeocoderResponseMetaData": {
+          "request": "volga river",
+          "found": "1",
+          "results": "10"
+        }
+      },
+      "featureMember": [
+        {
+          "GeoObject": {
+            "metaDataProperty": {
+              "GeocoderMetaData": {
+                "kind": "hydro",
+                "text": "Russia, Volga River",
+                "precision": "other",
+                "Address": {
+                  "country_code": "RU",
+                  "formatted": "Volga River",
+                  "Components": [
+                    {
+                      "kind": "country",
+                      "name": "Russia"
+                    },
+                    {
+                      "kind": "hydro",
+                      "name": "Volga River"
+                    }
+                  ]
+                },
+                "AddressDetails": {
+                  "Country": {
+                    "AddressLine": "Volga River",
+                    "CountryNameCode": "RU",
+                    "CountryName": "Russia",
+                    "Locality": {
+                      "Premise": {
+                        "PremiseName": "Volga River"
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "description": "Russia",
+            "name": "Volga River",
+            "boundedBy": {
+              "Envelope": {
+                "lowerCorner": "32.468241 45.697053",
+                "upperCorner": "50.181608 58.194645"
+              }
+            },
+            "Point": {
+              "pos": "45.139984 49.550996"
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/test/mongoid_test_helper.rb b/test/mongoid_test_helper.rb
new file mode 100644
index 0000000..ef072fb
--- /dev/null
+++ b/test/mongoid_test_helper.rb
@@ -0,0 +1,102 @@
+require 'rubygems'
+require 'test/unit'
+require 'test_helper'
+require 'mongoid'
+require 'geocoder/models/mongoid'
+
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+if (::Mongoid::VERSION >= "3")
+  Mongoid.logger = Logger.new($stderr, :debug)
+else
+  Mongoid.configure do |config|
+    config.logger = Logger.new($stderr, :debug)
+  end
+end
+
+##
+# Geocoded model.
+#
+class PlaceUsingMongoid
+  include Mongoid::Document
+  include Geocoder::Model::Mongoid
+
+  geocoded_by :address, :coordinates => :location
+  field :name
+  field :address
+  field :location, :type => Array
+
+  def initialize(name, address)
+    super()
+    write_attribute :name, name
+    write_attribute :address, address
+  end
+end
+
+class PlaceUsingMongoidWithoutIndex
+  include Mongoid::Document
+  include Geocoder::Model::Mongoid
+
+  field :location, :type => Array
+  geocoded_by :location, :skip_index => true
+end
+
+class PlaceUsingMongoidReverseGeocoded
+  include Mongoid::Document
+  include Geocoder::Model::Mongoid
+
+  field :address
+  field :coordinates, :type => Array
+  reverse_geocoded_by :coordinates
+
+  def initialize(name, latitude, longitude)
+    super()
+    write_attribute :name, name
+    write_attribute :coordinates, [latitude, longitude]
+  end
+end
+
+class PlaceUsingMongoidWithCustomResultsHandling
+  include Mongoid::Document
+  include Geocoder::Model::Mongoid
+
+  field :location, :type => Array
+  field :coords_string
+  field :name
+  field :address
+  geocoded_by :address, :coordinates => :location do |obj,results|
+    if result = results.first
+      obj.coords_string = "#{result.latitude},#{result.longitude}"
+    else
+      obj.coords_string = "NOT FOUND"
+    end
+  end
+
+  def initialize(name, address)
+    super()
+    write_attribute :name, name
+    write_attribute :address, address
+  end
+end
+
+class PlaceUsingMongoidReverseGeocodedWithCustomResultsHandling
+  include Mongoid::Document
+  include Geocoder::Model::Mongoid
+
+  field :name
+  field :country
+  field :coordinates, :type => Array
+
+  reverse_geocoded_by :coordinates do |obj,results|
+    if result = results.first
+      obj.country = result.country_code
+    end
+  end
+
+  def initialize(name, latitude, longitude)
+    super()
+    write_attribute :name, name
+    write_attribute :coordinates, [latitude, longitude]
+  end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..c6cd041
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,777 @@
+# encoding: utf-8
+require 'rubygems'
+require 'test/unit'
+
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'yaml'
+configs = YAML.load_file('test/database.yml')
+
+if configs.keys.include? ENV['DB']
+  require 'active_record'
+
+  # Establish a database connection
+  ActiveRecord::Base.configurations = configs
+
+  db_name = ENV['DB']
+  if db_name == 'sqlite' && ENV['USE_SQLITE_EXT'] == '1' then
+    gem 'sqlite_ext'
+    require 'sqlite_ext'
+    SqliteExt.register_ruby_math
+  end
+  ActiveRecord::Base.establish_connection(db_name.to_sym)
+  ActiveRecord::Base.default_timezone = :utc
+
+  if defined? ActiveRecord::MigrationContext
+    if ActiveRecord.version.release < Gem::Version.new('6.0.0')
+      # ActiveRecord >=5.2, takes one argument
+      ActiveRecord::MigrationContext.new('test/db/migrate').migrate
+    else
+      # ActiveRecord >=6.0, takes two arguments
+      ActiveRecord::MigrationContext.new('test/db/migrate', nil).migrate
+    end
+  else
+    ActiveRecord::Migrator.migrate('test/db/migrate', nil)
+  end
+
+else
+  class MysqlConnection
+    def adapter_name
+      "mysql"
+    end
+  end
+
+  ##
+  # Simulate enough of ActiveRecord::Base that objects can be used for testing.
+  #
+  module ActiveRecord
+    class Base
+
+      def initialize
+        @attributes = {}
+      end
+
+      def read_attribute(attr_name)
+        @attributes[attr_name.to_sym]
+      end
+
+      def write_attribute(attr_name, value)
+        @attributes[attr_name.to_sym] = value
+      end
+
+      def update_attribute(attr_name, value)
+        write_attribute(attr_name.to_sym, value)
+      end
+
+      def self.scope(*args); end
+
+      def self.connection
+        MysqlConnection.new
+      end
+
+      def method_missing(name, *args, &block)
+        if name.to_s[-1..-1] == "="
+          write_attribute name.to_s[0...-1], *args
+        else
+          read_attribute name
+        end
+      end
+
+      class << self
+        def table_name
+          'test_table_name'
+        end
+
+        def primary_key
+          :id
+        end
+
+        def maximum(_field)
+          1.0
+        end
+      end
+    end
+  end
+end
+
+# simulate Rails module so Railtie gets loaded
+module Rails
+end
+
+# Require Geocoder after ActiveRecord simulator.
+require 'geocoder'
+require 'geocoder/lookups/base'
+
+# and initialize Railtie manually (since Rails::Railtie doesn't exist)
+Geocoder::Railtie.insert
+
+##
+# Mock HTTP request to geocoding service.
+#
+module Geocoder
+  module Lookup
+    class Base
+      private
+      def fixture_exists?(filename)
+        File.exist?(File.join("test", "fixtures", filename))
+      end
+
+      def read_fixture(file)
+        filepath = File.join("test", "fixtures", file)
+        s = File.read(filepath).strip.gsub(/\n\s*/, "")
+        MockHttpResponse.new(body: s, code: "200")
+      end
+
+      ##
+      # Fixture to use if none match the given query.
+      #
+      def default_fixture_filename
+        "#{fixture_prefix}_madison_square_garden"
+      end
+
+      def fixture_prefix
+        handle
+      end
+
+      def fixture_for_query(query)
+        label = query.reverse_geocode? ? "reverse" : query.text.gsub(/[ \.]/, "_")
+        filename = "#{fixture_prefix}_#{label}"
+        fixture_exists?(filename) ? filename : default_fixture_filename
+      end
+
+      # This alias allows us to use this method in further tests
+      # to actually test http requests
+      alias_method :actual_make_api_request, :make_api_request
+      remove_method(:make_api_request)
+
+      def make_api_request(query)
+        raise Timeout::Error if query.text == "timeout"
+        raise SocketError if query.text == "socket_error"
+        raise Errno::ECONNREFUSED if query.text == "connection_refused"
+        raise Errno::EHOSTUNREACH if query.text == "host_unreachable"
+        if query.text == "invalid_json"
+          return MockHttpResponse.new(:body => 'invalid json', :code => 200)
+        end
+
+        read_fixture fixture_for_query(query)
+      end
+    end
+
+    require 'geocoder/lookups/bing'
+    class Bing
+      private
+      def read_fixture(file)
+        if file == "bing_service_unavailable"
+          filepath = File.join("test", "fixtures", file)
+          s = File.read(filepath).strip.gsub(/\n\s*/, "")
+          MockHttpResponse.new(body: s, code: "200", headers: {'x-ms-bm-ws-info' => "1"})
+        else
+          super
+        end
+      end
+    end
+
+    require 'geocoder/lookups/db_ip_com'
+    class DbIpCom
+      private
+      def fixture_prefix
+        "db_ip_com"
+      end
+    end
+
+    require 'geocoder/lookups/google_premier'
+    class GooglePremier
+      private
+      def fixture_prefix
+        "google"
+      end
+    end
+
+    require 'geocoder/lookups/google_places_details'
+    class GooglePlacesDetails
+      private
+      def fixture_prefix
+        "google_places_details"
+      end
+    end
+
+    require 'geocoder/lookups/dstk'
+    class Dstk
+      private
+      def fixture_prefix
+        "google"
+      end
+    end
+
+    require 'geocoder/lookups/location_iq'
+    class LocationIq
+      private
+      def fixture_prefix
+        "location_iq"
+      end
+    end
+
+    require 'geocoder/lookups/yandex'
+    class Yandex
+      private
+      def default_fixture_filename
+        "yandex_kremlin"
+      end
+    end
+
+    require 'geocoder/lookups/abstract_api'
+    class AbstractApi
+      private
+      def default_fixture_filename
+        "abstract_api"
+      end
+    end
+
+    require 'geocoder/lookups/freegeoip'
+    class Freegeoip
+      private
+      def default_fixture_filename
+        "freegeoip_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/ipbase'
+    class Ipbase
+      private
+      def default_fixture_filename
+        "ipbase_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/ip2location'
+    class Ip2location
+      private
+      def default_fixture_filename
+        "ip2location_8_8_8_8"
+      end
+    end
+
+    require 'geocoder/lookups/ipgeolocation'
+    class Ipgeolocation
+      private
+      def default_fixture_filename
+        "ipgeolocation_103_217_177_217"
+      end
+    end
+
+    require 'geocoder/lookups/ipqualityscore'
+    class Ipqualityscore
+      private
+      def default_fixture_filename
+        "ipqualityscore_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/ipstack'
+    class Ipstack
+      private
+      def default_fixture_filename
+        "ipstack_134_201_250_155"
+      end
+    end
+
+    require 'geocoder/lookups/geoip2'
+    class Geoip2
+      private
+
+      remove_method(:results)
+
+      def results(query)
+        return [] if query.to_s == 'no results'
+        return [] if query.to_s == '127.0.0.1'
+        [{'city'=>{'names'=>{'en'=>'Mountain View', 'ru'=>'Маунтин-Вью'}},'country'=>{'iso_code'=>'US','names'=>
+        {'en'=>'United States'}},'location'=>{'latitude'=>37.41919999999999,
+        'longitude'=>-122.0574},'postal'=>{'code'=>'94043'},'subdivisions'=>[{
+        'iso_code'=>'CA','names'=>{'en'=>'California'}}]}]
+      end
+
+      def default_fixture_filename
+        'geoip2_74_200_247_59'
+      end
+    end
+
+    require 'geocoder/lookups/telize'
+    class Telize
+      private
+      def default_fixture_filename
+        "telize_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/pointpin'
+    class Pointpin
+      private
+      def default_fixture_filename
+        "pointpin_80_111_55_55"
+      end
+    end
+
+    require 'geocoder/lookups/maxmind'
+    class Maxmind
+      private
+      def default_fixture_filename
+        "maxmind_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/maxmind_geoip2'
+    class MaxmindGeoip2
+      private
+      def default_fixture_filename
+        "maxmind_geoip2_1_2_3_4"
+      end
+    end
+
+    require 'geocoder/lookups/maxmind_local'
+    class MaxmindLocal
+      private
+
+      remove_method(:results)
+
+      def results query
+        return [] if query.to_s == "no results"
+
+        if query.to_s == '127.0.0.1'
+          []
+        else
+          [{:request=>"8.8.8.8", :ip=>"8.8.8.8", :country_code2=>"US", :country_code3=>"USA", :country_name=>"United States", :continent_code=>"NA", :region_name=>"CA", :city_name=>"Mountain View", :postal_code=>"94043", :latitude=>37.41919999999999, :longitude=>-122.0574, :dma_code=>807, :area_code=>650, :timezone=>"America/Los_Angeles"}]
+        end
+      end
+    end
+
+
+    require 'geocoder/lookups/baidu'
+    class Baidu
+      private
+      def default_fixture_filename
+        "baidu_shanghai_pearl_tower"
+      end
+    end
+
+    require 'geocoder/lookups/nationaal_georegister_nl'
+    class NationaalGeoregisterNl
+      private
+      def default_fixture_filename
+        "nationaal_georegister_nl"
+      end
+    end
+
+    require 'geocoder/lookups/baidu_ip'
+    class BaiduIp
+      private
+      def default_fixture_filename
+        "baidu_ip_202_198_16_3"
+      end
+    end
+
+    require 'geocoder/lookups/tencent'
+    class Tencent
+      private
+      def default_fixture_filename
+        "tencent_shanghai_pearl_tower"
+      end
+    end
+
+    require 'geocoder/lookups/geocodio'
+    class Geocodio
+      private
+      def default_fixture_filename
+        "geocodio_1101_pennsylvania_ave"
+      end
+    end
+
+    require 'geocoder/lookups/melissa_street'
+    class MelissaStreet
+      private
+      def default_fixture_filename
+        "melissa_street_oakland_city_hall"
+      end
+    end
+
+    require 'geocoder/lookups/postcode_anywhere_uk'
+    class PostcodeAnywhereUk
+      private
+      def fixture_prefix
+        'postcode_anywhere_uk_geocode_v2_00'
+      end
+
+      def default_fixture_filename
+        "#{fixture_prefix}_romsey"
+      end
+    end
+
+    require 'geocoder/lookups/postcodes_io'
+    class PostcodesIo
+      private
+      def fixture_prefix
+        'postcodes_io'
+      end
+
+      def default_fixture_filename
+        "#{fixture_prefix}_malvern_hills"
+      end
+    end
+
+    require 'geocoder/lookups/uk_ordnance_survey_names'
+    class Geocoder::Lookup::UkOrdnanceSurveyNames
+      private
+      def default_fixture_filename
+        "#{fixture_prefix}_london"
+      end
+    end
+
+    require 'geocoder/lookups/geoportail_lu'
+    class GeoportailLu
+      private
+      def fixture_prefix
+        "geoportail_lu"
+      end
+
+      def default_fixture_filename
+        "#{fixture_prefix}_boulevard_royal"
+      end
+    end
+
+    require 'geocoder/lookups/latlon'
+    class Latlon
+      private
+      def default_fixture_filename
+        "latlon_6000_universal_blvd"
+      end
+    end
+
+    require 'geocoder/lookups/ipinfo_io'
+    class IpinfoIo
+      private
+      def default_fixture_filename
+        "ipinfo_io_8_8_8_8"
+      end
+    end
+
+    require 'geocoder/lookups/ipregistry'
+    class Ipregistry
+      private
+      def default_fixture_filename
+        "ipregistry_8_8_8_8"
+      end
+    end
+
+    require 'geocoder/lookups/ipapi_com'
+    class IpapiCom
+      private
+      def default_fixture_filename
+        "ipapi_com_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/ipdata_co'
+    class IpdataCo
+      private
+      def default_fixture_filename
+        "ipdata_co_74_200_247_59"
+      end
+    end
+
+    require 'geocoder/lookups/ban_data_gouv_fr'
+    class BanDataGouvFr
+      private
+      def fixture_prefix
+        "ban_data_gouv_fr"
+      end
+
+      def default_fixture_filename
+        "#{fixture_prefix}_rue_yves_toudic"
+      end
+    end
+
+    require 'geocoder/lookups/amap'
+    class Amap
+      private
+      def default_fixture_filename
+        "amap_shanghai_pearl_tower"
+      end
+    end
+
+    require 'geocoder/lookups/pickpoint'
+    class Pickpoint
+      private
+      def fixture_prefix
+        "pickpoint"
+      end
+    end
+
+    require 'geocoder/lookups/twogis'
+    class Twogis
+      private
+      def default_fixture_filename
+        "twogis_kremlin"
+      end
+    end
+
+    require 'geocoder/lookups/amazon_location_service'
+    MockResults = Struct.new(:results)
+    MockAWSPlaceGeometry = Struct.new(:point)
+
+    MockAWSPlace = Struct.new(*%i[
+      address_number country geometry label municipality neighborhood postal_code region street sub_region
+    ])
+    class MockAWSPlace
+      def place
+        self
+      end
+    end
+
+    class MockAmazonLocationServiceClient
+      def search_place_index_for_position(params = {}, options = {})
+        # Amazon transposes latitude and longitude, so our client does too on the outbound call and inbound data
+        return mock_results if params[:position] == ["-75.676333", "45.423733"]
+        mock_no_results
+      end
+
+      def search_place_index_for_text(params = {}, options = {})
+        return mock_results if params[:text].include? "Madison Square Garden"
+        mock_no_results
+      end
+
+      private
+
+      def fixture
+        eval File.read File.join("test", "fixtures", "amazon_location_service_madison_square_garden")
+      end
+
+      def mock_results
+        MockResults.new([MockAWSPlace.new(*fixture)])
+      end
+
+      def mock_no_results
+        MockResults.new([])
+      end
+    end
+
+    class AmazonLocationService
+      private
+      def client
+        MockAmazonLocationServiceClient.new
+      end
+    end
+
+    require 'geocoder/lookups/geoapify'
+    class Geoapify
+      private
+      def read_fixture(file)
+        filepath = File.join("test", "fixtures", file)
+        s = File.read(filepath).strip.gsub(/\n\s*/, "")
+
+        options = { body: s, code: 200 }
+        if file == "geoapify_invalid_request"
+          options[:code] = 500
+        elsif file == "geoapify_invalid_key"
+          options[:code] = 401
+        end
+
+        MockHttpResponse.new(options)
+      end
+    end
+
+    require 'geocoder/lookups/photon'
+    class Photon
+      private
+      def read_fixture(file)
+        filepath = File.join("test", "fixtures", file)
+        s = File.read(filepath).strip.gsub(/\n\s*/, "")
+
+        options = { body: s, code: 200 }
+        if file == "photon_invalid_request"
+          options[:code] = 400
+        end
+
+        MockHttpResponse.new(options)
+      end
+    end
+  end
+end
+
+##
+# Geocoded model.
+#
+class Place < ActiveRecord::Base
+  geocoded_by :address
+
+  def initialize(name, address)
+    super()
+    write_attribute :name, name
+    write_attribute :address, address
+  end
+end
+
+##
+# Geocoded model.
+# - Has user-defined primary key (not just 'id')
+#
+class PlaceWithCustomPrimaryKey < Place
+
+  class << self
+    def primary_key
+      :custom_primary_key_id
+    end
+  end
+
+end
+
+class PlaceReverseGeocoded < ActiveRecord::Base
+  reverse_geocoded_by :latitude, :longitude
+
+  def initialize(name, latitude, longitude)
+    super()
+    write_attribute :name, name
+    write_attribute :latitude, latitude
+    write_attribute :longitude, longitude
+  end
+end
+
+class PlaceWithCustomResultsHandling < ActiveRecord::Base
+  geocoded_by :address do |obj,results|
+    if result = results.first
+      obj.coords_string = "#{result.latitude},#{result.longitude}"
+    else
+      obj.coords_string = "NOT FOUND"
+    end
+  end
+
+  def initialize(name, address)
+    super()
+    write_attribute :name, name
+    write_attribute :address, address
+  end
+end
+
+class PlaceReverseGeocodedWithCustomResultsHandling < ActiveRecord::Base
+  reverse_geocoded_by :latitude, :longitude do |obj,results|
+    if result = results.first
+      obj.country = result.country_code
+    end
+  end
+
+  def initialize(name, latitude, longitude)
+    super()
+    write_attribute :name, name
+    write_attribute :latitude, latitude
+    write_attribute :longitude, longitude
+  end
+end
+
+class PlaceWithForwardAndReverseGeocoding < ActiveRecord::Base
+  geocoded_by :address, :latitude => :lat, :longitude => :lon
+  reverse_geocoded_by :lat, :lon, :address => :location
+
+  def initialize(name)
+    super()
+    write_attribute :name, name
+  end
+end
+
+class PlaceWithCustomLookup < ActiveRecord::Base
+  geocoded_by :address, :lookup => :nominatim do |obj,results|
+    if result = results.first
+      obj.result_class = result.class
+    end
+  end
+
+  def initialize(name, address)
+    super()
+    write_attribute :name, name
+    write_attribute :address, address
+  end
+end
+
+class PlaceWithCustomLookupProc < ActiveRecord::Base
+  geocoded_by :address, :lookup => lambda{|obj| obj.custom_lookup } do |obj,results|
+    if result = results.first
+      obj.result_class = result.class
+    end
+  end
+
+  def custom_lookup
+    :nominatim
+  end
+
+  def initialize(name, address)
+    super()
+    write_attribute :name, name
+    write_attribute :address, address
+  end
+end
+
+class PlaceReverseGeocodedWithCustomLookup < ActiveRecord::Base
+  reverse_geocoded_by :latitude, :longitude, :lookup => :nominatim do |obj,results|
+    if result = results.first
+      obj.result_class = result.class
+    end
+  end
+
+  def initialize(name, latitude, longitude)
+    super()
+    write_attribute :name, name
+    write_attribute :latitude, latitude
+    write_attribute :longitude, longitude
+  end
+end
+
+
+class GeocoderTestCase < Test::Unit::TestCase
+  self.test_order = :random
+
+  def setup
+    super
+    Geocoder::Configuration.initialize
+    Geocoder.configure(
+      :maxmind => {:service => :city_isp_org},
+      :maxmind_geoip2 => {:service => :insights, :basic_auth => {:user => "user", :password => "password"}})
+  end
+
+  def geocoded_object_params(abbrev)
+    {
+      :msg => ["Madison Square Garden", "4 Penn Plaza, New York, NY"]
+    }[abbrev]
+  end
+
+  def reverse_geocoded_object_params(abbrev)
+    {
+      :msg => ["Madison Square Garden", 40.750354, -73.993371]
+    }[abbrev]
+  end
+
+  def set_api_key!(lookup_name)
+    lookup = Geocoder::Lookup.get(lookup_name)
+    if lookup.required_api_key_parts.size == 1
+      key = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+    elsif lookup.required_api_key_parts.size > 1
+      key = lookup.required_api_key_parts
+    else
+      key = nil
+    end
+    Geocoder.configure(:api_key => key)
+  end
+end
+
+class MockHttpResponse
+  attr_reader :code, :body
+  def initialize(options = {})
+    @code = options[:code].to_s
+    @body = options[:body]
+    @headers = options[:headers] || {}
+  end
+
+  def [](key)
+    @headers[key]
+  end
+end
+
+module MockLookup
+end
diff --git a/test/unit/active_record_test.rb b/test/unit/active_record_test.rb
new file mode 100644
index 0000000..3392765
--- /dev/null
+++ b/test/unit/active_record_test.rb
@@ -0,0 +1,15 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ActiveRecordTest < GeocoderTestCase
+
+  def test_exclude_condition_when_model_has_a_custom_primary_key
+    venue = PlaceWithCustomPrimaryKey.new(*geocoded_object_params(:msg))
+
+    # just call private method directly so we don't have to stub .near scope
+    conditions = venue.class.send(:add_exclude_condition, ["fake_condition"], venue)
+
+    assert_match( /#{PlaceWithCustomPrimaryKey.primary_key}/, conditions.join)
+  end
+
+end
diff --git a/test/unit/cache_test.rb b/test/unit/cache_test.rb
new file mode 100644
index 0000000..f58bc4f
--- /dev/null
+++ b/test/unit/cache_test.rb
@@ -0,0 +1,73 @@
+# encoding: utf-8
+require 'test_helper'
+
+class CacheTest < GeocoderTestCase
+  def setup
+    @tempfile = Tempfile.new("log")
+    @logger = Logger.new(@tempfile.path)
+    Geocoder.configure(logger: @logger)
+  end
+
+  def teardown
+    Geocoder.configure(logger: :kernel)
+    @logger.close
+    @tempfile.close
+  end
+
+  def test_second_occurrence_of_request_is_cache_hit
+    Geocoder.configure(:cache => {})
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      next if
+        # local, does not use cache
+        l == :maxmind_local ||
+        l == :geoip2 ||
+        # uses the AWS gem, not HTTP requests with caching
+        l == :amazon_location_service
+      Geocoder.configure(:lookup => l)
+      set_api_key!(l)
+      results = Geocoder.search("Madison Square Garden")
+      assert !results.first.cache_hit,
+        "Lookup #{l} returned erroneously cached result."
+      results = Geocoder.search("Madison Square Garden")
+      assert results.first.cache_hit,
+        "Lookup #{l} did not return cached result."
+    end
+  end
+
+  def test_google_over_query_limit_does_not_hit_cache
+    Geocoder.configure(:cache => {})
+    Geocoder.configure(:lookup => :google)
+    set_api_key!(:google)
+    Geocoder.configure(:always_raise => :all)
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search("over limit")
+    end
+    lookup = Geocoder::Lookup.get(:google)
+    assert_equal false, lookup.instance_variable_get(:@cache_hit)
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search("over limit")
+    end
+    assert_equal false, lookup.instance_variable_get(:@cache_hit)
+  end
+
+  def test_bing_service_unavailable_without_raising_does_not_hit_cache
+    Geocoder.configure(cache: {}, lookup: :bing, always_raise: [])
+    set_api_key!(:bing)
+    lookup = Geocoder::Lookup.get(:bing)
+
+    Geocoder.search("service unavailable")
+    assert_false lookup.instance_variable_get(:@cache_hit)
+
+    Geocoder.search("service unavailable")
+    assert_false lookup.instance_variable_get(:@cache_hit)
+  end
+
+  def test_expire_all_urls
+    Geocoder.configure(cache: {}, cache_options: {prefix: "geocoder:"})
+    lookup = Geocoder::Lookup.get(:nominatim)
+    lookup.cache['http://api.nominatim.com/'] = 'data'
+    assert_operator 0, :<, lookup.cache.send(:keys).size
+    lookup.cache.expire(:all)
+    assert_equal 0, lookup.cache.send(:keys).size
+  end
+end
diff --git a/test/unit/calculations_test.rb b/test/unit/calculations_test.rb
new file mode 100644
index 0000000..9043001
--- /dev/null
+++ b/test/unit/calculations_test.rb
@@ -0,0 +1,226 @@
+# encoding: utf-8
+require 'test_helper'
+
+class CalculationsTest < GeocoderTestCase
+  def setup
+    Geocoder.configure(
+      :units => :mi,
+      :distances => :linear
+    )
+  end
+
+  # --- degree distance ---
+
+  def test_longitude_degree_distance_at_equator
+    assert_equal 69, Geocoder::Calculations.longitude_degree_distance(0).round
+  end
+
+  def test_longitude_degree_distance_at_new_york
+    assert_equal 53, Geocoder::Calculations.longitude_degree_distance(40).round
+  end
+
+  def test_longitude_degree_distance_at_north_pole
+    assert_equal 0, Geocoder::Calculations.longitude_degree_distance(89.98).round
+  end
+
+
+  # --- distance between ---
+
+  def test_distance_between_in_miles
+    assert_equal 69, Geocoder::Calculations.distance_between([0,0], [0,1]).round
+    la_to_ny = Geocoder::Calculations.distance_between([34.05,-118.25], [40.72,-74]).round
+    assert (la_to_ny - 2444).abs < 10
+  end
+
+  def test_distance_between_in_kilometers
+    assert_equal 111, Geocoder::Calculations.distance_between([0,0], [0,1], :units => :km).round
+    la_to_ny = Geocoder::Calculations.distance_between([34.05,-118.25], [40.72,-74], :units => :km).round
+    assert (la_to_ny - 3942).abs < 10
+  end
+
+  def test_distance_between_in_nautical_miles
+    assert_equal 60, Geocoder::Calculations.distance_between([0,0], [0,1], :units => :nm).round
+    la_to_ny = Geocoder::Calculations.distance_between([34.05,-118.25], [40.72,-74], :units => :nm).round
+    assert (la_to_ny - 2124).abs < 10
+  end
+
+
+  # --- geographic center ---
+
+  def test_geographic_center_with_arrays
+    assert_equal [0.0, 0.5],
+      Geocoder::Calculations.geographic_center([[0,0], [0,1]])
+    assert_equal [0.0, 1.0],
+      Geocoder::Calculations.geographic_center([[0,0], [0,1], [0,2]])
+  end
+
+  def test_geographic_center_with_mixed_arguments
+    p1 = [0, 0]
+    p2 = PlaceReverseGeocoded.new("Some Cold Place", 0, 1)
+    assert_equal [0.0, 0.5], Geocoder::Calculations.geographic_center([p1, p2])
+  end
+
+
+  # --- bounding box ---
+
+  def test_bounding_box_calculation_in_miles
+    center = [51, 7] # Cologne, DE
+    radius = 10 # miles
+    corners = [50.86, 6.77, 51.14, 7.23]
+    assert_equal corners.map{ |i| (i * 100).round },
+      Geocoder::Calculations.bounding_box(center, radius).map{ |i| (i * 100).round }
+  end
+
+  def test_bounding_box_calculation_in_kilometers
+    center = [51, 7] # Cologne, DE
+    radius = 111 # kilometers (= 1 degree latitude)
+    corners = [50, 5.41, 52, 8.59]
+    assert_equal corners.map{ |i| (i * 100).round },
+      Geocoder::Calculations.bounding_box(center, radius, :units => :km).map{ |i| (i * 100).round }
+  end
+
+  def test_bounding_box_calculation_with_object
+    center = [51, 7] # Cologne, DE
+    radius = 10 # miles
+    corners = [50.86, 6.77, 51.14, 7.23]
+    obj = PlaceReverseGeocoded.new("Cologne", center[0], center[1])
+    assert_equal corners.map{ |i| (i * 100).round },
+      Geocoder::Calculations.bounding_box(obj, radius).map{ |i| (i * 100).round }
+  end
+
+  def test_bounding_box_calculation_with_address_string
+    assert_nothing_raised do
+      Geocoder::Calculations.bounding_box("4893 Clay St, San Francisco, CA", 50)
+    end
+  end
+
+  # --- random point ---
+
+  def test_random_point_within_radius
+    20.times do
+      center = [51, 7] # Cologne, DE
+      radius = 10 # miles
+      random_point = Geocoder::Calculations.random_point_near(center, radius)
+      distance = Geocoder::Calculations.distance_between(center, random_point)
+      assert distance <= radius
+    end
+  end
+
+  # --- bearing ---
+
+  def test_compass_points
+    assert_equal "N",  Geocoder::Calculations.compass_point(0)
+    assert_equal "N",  Geocoder::Calculations.compass_point(1.0)
+    assert_equal "N",  Geocoder::Calculations.compass_point(360)
+    assert_equal "N",  Geocoder::Calculations.compass_point(361)
+    assert_equal "N",  Geocoder::Calculations.compass_point(-22)
+    assert_equal "NW", Geocoder::Calculations.compass_point(-23)
+    assert_equal "S",  Geocoder::Calculations.compass_point(180)
+    assert_equal "S",  Geocoder::Calculations.compass_point(181)
+  end
+
+  def test_bearing_between
+    bearings = {
+      :n => 0,
+      :e => 90,
+      :s => 180,
+      :w => 270
+    }
+    points = {
+      :n => [41, -75],
+      :e => [40, -74],
+      :s => [39, -75],
+      :w => [40, -76]
+    }
+    directions = [:n, :e, :s, :w]
+    methods = [:linear, :spherical]
+
+    methods.each do |m|
+      directions.each_with_index do |d,i|
+        opp = directions[(i + 2) % 4] # opposite direction
+        b = Geocoder::Calculations.bearing_between(
+          points[d], points[opp], :method => m)
+        assert (b - bearings[opp]).abs < 1,
+          "Bearing (#{m}) should be close to #{bearings[opp]} but was #{b}."
+      end
+    end
+  end
+
+  def test_spherical_bearing_to
+    l = PlaceReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    assert_equal 324, l.bearing_to([50,-85], :method => :spherical).round
+  end
+
+  def test_spherical_bearing_from
+    l = PlaceReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    assert_equal 136, l.bearing_from([50,-85], :method => :spherical).round
+  end
+
+  def test_linear_bearing_from_and_to_are_exactly_opposite
+    l = PlaceReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    assert_equal l.bearing_from([50,-86.1]), l.bearing_to([50,-86.1]) - 180
+  end
+
+  def test_extract_coordinates_when_integers
+    coords = [-23, 47]
+    l = PlaceReverseGeocoded.new("Madagascar", coords[0], coords[1])
+    assert_equal coords.map(&:to_f), Geocoder::Calculations.extract_coordinates(l)
+    assert_equal coords.map(&:to_f), Geocoder::Calculations.extract_coordinates(coords)
+  end
+
+  def test_extract_coordinates_when_strings
+    coords = ["-23.1", "47.2"]
+    l = PlaceReverseGeocoded.new("Madagascar", coords[0], coords[1])
+    assert_equal coords.map(&:to_f), Geocoder::Calculations.extract_coordinates(l)
+    assert_equal coords.map(&:to_f), Geocoder::Calculations.extract_coordinates(coords)
+  end
+
+  def test_extract_coordinates_when_nan
+    result = Geocoder::Calculations.extract_coordinates([ nil, nil ])
+    assert_nan_coordinates?(result)
+
+    result = Geocoder::Calculations.extract_coordinates(nil)
+    assert_nan_coordinates?(result)
+
+    result = Geocoder::Calculations.extract_coordinates('')
+    assert_nan_coordinates?(result)
+
+    result = Geocoder::Calculations.extract_coordinates([ 'nix' ])
+    assert_nan_coordinates?(result)
+
+    o = Object.new
+    result = Geocoder::Calculations.extract_coordinates(o)
+    assert_nan_coordinates?(result)
+  end
+
+  def test_coordinates_present
+    assert Geocoder::Calculations.coordinates_present?(3.23)
+    assert !Geocoder::Calculations.coordinates_present?(nil)
+    assert !Geocoder::Calculations.coordinates_present?(Geocoder::Calculations::NAN)
+    assert !Geocoder::Calculations.coordinates_present?(3.23, nil)
+  end
+
+  private # ------------------------------------------------------------------
+
+  def assert_nan_coordinates?(value)
+    assert value.is_a?(Array) &&
+      value.size == 2 &&
+      value[0].nan? &&
+      value[1].nan?,
+      "Expected value to be [NaN, NaN] but was #{value}"
+  end
+
+  def test_endpoint
+    # test 5 time with random coordinates and headings
+    [0..5].each do |i|
+      rheading = [*0..359].sample
+      rdistance = [*0..100].sample
+      startpoint = [45.0906, 7.6596]
+      endpoint = Geocoder::Calculations.endpoint(startpoint, rheading, rdistance)
+      assert_in_delta rdistance, 
+        Geocoder::Calculations.distance_between(startpoint, endpoint, :method => :spherical), 1E-5
+      assert_in_delta rheading, 
+        Geocoder::Calculations.bearing_between(startpoint, endpoint, :method => :spherical), 1E-2
+    end
+  end
+end
diff --git a/test/unit/configuration_test.rb b/test/unit/configuration_test.rb
new file mode 100644
index 0000000..df8aed9
--- /dev/null
+++ b/test/unit/configuration_test.rb
@@ -0,0 +1,73 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ConfigurationTest < GeocoderTestCase
+  def setup
+    Geocoder::Configuration.set_defaults
+  end
+
+  def test_exception_raised_on_bad_lookup_config
+    Geocoder.configure(:lookup => :stoopid)
+    assert_raises Geocoder::ConfigurationError do
+      Geocoder.search "something dumb"
+    end
+  end
+
+  def test_setting_with_class_method
+    Geocoder::Configuration.units = :test
+    assert_equal :test, Geocoder.config.units
+  end
+
+  def test_setting_with_configure_method
+    Geocoder.configure(:units => :test)
+    assert_equal :test, Geocoder.config.units
+  end
+
+  def test_config_for_lookup
+    Geocoder.configure(
+      :timeout => 5,
+      :api_key => "aaa",
+      :google => {
+        :timeout => 2
+      }
+    )
+    assert_equal 2, Geocoder.config_for_lookup(:google).timeout
+    assert_equal "aaa", Geocoder.config_for_lookup(:google).api_key
+  end
+
+  def test_configuration_chain
+    v = PlaceReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    v.latitude  = 0
+    v.longitude = 0
+
+    # method option > global configuration
+    Geocoder.configure(:units => :km)
+    assert_equal 69, v.distance_to([0,1], :mi).round
+
+    # per-model configuration > global configuration
+    PlaceReverseGeocoded.reverse_geocoded_by :latitude, :longitude, method: :spherical, units: :mi
+    assert_equal 69, v.distance_to([0,1]).round
+
+    # method option > per-model configuration
+    assert_equal 111, v.distance_to([0,1], :km).round
+  end
+
+  def test_merge_into_lookup_config
+    base = {
+      timeout: 5,
+      api_key: "xxx"
+    }
+    new = {
+      timeout: 10,
+      units: :km,
+    }
+    merged = {
+      timeout: 10, # overwritten
+      units: :km, # added
+      api_key: "xxx" # preserved
+    }
+    Geocoder.configure(google: base)
+    Geocoder.merge_into_lookup_config(:google, new)
+    assert_equal merged, Geocoder.config[:google]
+  end
+end
diff --git a/test/unit/error_handling_test.rb b/test/unit/error_handling_test.rb
new file mode 100644
index 0000000..c63ef2f
--- /dev/null
+++ b/test/unit/error_handling_test.rb
@@ -0,0 +1,90 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ErrorHandlingTest < GeocoderTestCase
+
+  def teardown
+    Geocoder.configure(:always_raise => [])
+  end
+
+  def test_does_not_choke_on_timeout
+    silence_warnings do
+      Geocoder::Lookup.all_services_with_http_requests.each do |l|
+        Geocoder.configure(:lookup => l)
+        set_api_key!(l)
+        assert_nothing_raised { Geocoder.search("timeout") }
+      end
+    end
+  end
+
+  def test_always_raise_response_parse_error
+    Geocoder.configure(:always_raise => [Geocoder::ResponseParseError])
+    [:freegeoip, :google, :ipdata_co].each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      assert_raises Geocoder::ResponseParseError do
+        lookup.send(:results, Geocoder::Query.new("invalid_json"))
+      end
+    end
+  end
+
+  def test_never_raise_response_parse_error
+    [:freegeoip, :google, :ipdata_co].each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      silence_warnings do
+        assert_nothing_raised do
+          lookup.send(:results, Geocoder::Query.new("invalid_json"))
+        end
+      end
+    end
+  end
+
+  def test_always_raise_timeout_error
+    Geocoder.configure(:always_raise => [Timeout::Error])
+    Geocoder::Lookup.all_services_with_http_requests.each do |l|
+      next if l == :maxmind_local || l == :geoip2 # local, does not use cache
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      assert_raises Timeout::Error do
+        lookup.send(:results, Geocoder::Query.new("timeout"))
+      end
+    end
+  end
+
+  def test_always_raise_socket_error
+    Geocoder.configure(:always_raise => [SocketError])
+    Geocoder::Lookup.all_services_with_http_requests.each do |l|
+      next if l == :maxmind_local || l == :geoip2 # local, does not use cache
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      assert_raises SocketError do
+        lookup.send(:results, Geocoder::Query.new("socket_error"))
+      end
+    end
+  end
+
+  def test_always_raise_connection_refused_error
+    Geocoder.configure(:always_raise => [Errno::ECONNREFUSED])
+    Geocoder::Lookup.all_services_with_http_requests.each do |l|
+      next if l == :maxmind_local || l == :geoip2 # local, does not use cache
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      assert_raises Errno::ECONNREFUSED do
+        lookup.send(:results, Geocoder::Query.new("connection_refused"))
+      end
+    end
+  end
+
+  def test_always_raise_host_unreachable_error
+    Geocoder.configure(:always_raise => [Errno::EHOSTUNREACH])
+    Geocoder::Lookup.all_services_with_http_requests.each do |l|
+      next if l == :maxmind_local || l == :geoip2 # local, does not use cache
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      assert_raises Errno::EHOSTUNREACH do
+        lookup.send(:results, Geocoder::Query.new("host_unreachable"))
+      end
+    end
+  end
+end
diff --git a/test/unit/geocoder_test.rb b/test/unit/geocoder_test.rb
new file mode 100644
index 0000000..87d38f5
--- /dev/null
+++ b/test/unit/geocoder_test.rb
@@ -0,0 +1,82 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GeocoderTest < GeocoderTestCase
+
+  def test_distance_to_returns_float
+    v = Place.new(*geocoded_object_params(:msg))
+    v.latitude, v.longitude = [40.750354, -73.993371]
+    assert (v.distance_to([30, -94])).is_a?(Float)
+  end
+
+  def test_coordinates_method_returns_array
+    assert Geocoder.coordinates("Madison Square Garden, New York, NY").is_a?(Array)
+  end
+
+  def test_address_method_returns_string
+    assert Geocoder.address([40.750354, -73.993371]).is_a?(String)
+  end
+
+  def test_geographic_center_doesnt_overwrite_argument_value
+    # test for the presence of a bug that was introduced in version 0.9.11
+    orig_points = [[52,8], [46,9], [42,5]]
+    points = orig_points.clone
+    Geocoder::Calculations.geographic_center(points)
+    assert_equal orig_points, points
+  end
+
+  def test_geocode_assigns_and_returns_coordinates
+    v = Place.new(*geocoded_object_params(:msg))
+    assert_equal [Float, Float], v.geocode.map(&:class)
+    assert_kind_of Numeric, v.latitude
+    assert_kind_of Numeric, v.longitude
+  end
+
+  def test_geocode_block_executed_when_no_results
+    v = PlaceWithCustomResultsHandling.new("Nowhere", "no results")
+    v.geocode
+    assert_equal "NOT FOUND", v.coords_string
+  end
+
+  def test_reverse_geocode_assigns_and_returns_address
+    v = PlaceReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    assert_match(/New York/, v.reverse_geocode)
+    assert_match(/New York/, v.address)
+  end
+
+  def test_forward_and_reverse_geocoding_on_same_model_works
+    g = PlaceWithForwardAndReverseGeocoding.new("Exxon")
+    g.address = "404 New St, Middletown, CT"
+    g.geocode
+    assert_not_nil g.lat
+    assert_not_nil g.lon
+
+    assert_nil g.location
+    g.reverse_geocode
+    assert_not_nil g.location
+  end
+
+  def test_geocode_with_custom_lookup_param
+    v = PlaceWithCustomLookup.new(*geocoded_object_params(:msg))
+    v.geocode
+    assert_equal "Geocoder::Result::Nominatim", v.result_class.to_s
+  end
+
+  def test_geocode_with_custom_lookup_proc_param
+    v = PlaceWithCustomLookupProc.new(*geocoded_object_params(:msg))
+    v.geocode
+    assert_equal "Geocoder::Result::Nominatim", v.result_class.to_s
+  end
+
+  def test_reverse_geocode_with_custom_lookup_param
+    v = PlaceReverseGeocodedWithCustomLookup.new(*reverse_geocoded_object_params(:msg))
+    v.reverse_geocode
+    assert_equal "Geocoder::Result::Nominatim", v.result_class.to_s
+  end
+
+  def test_default_geocoder_caching_config
+    assert_nil Geocoder.config[:cache]
+    assert_nil Geocoder.config[:cache_options][:expiration]
+    assert_equal 'geocoder:', Geocoder.config[:cache_options][:prefix]
+  end
+end
diff --git a/test/unit/https_test.rb b/test/unit/https_test.rb
new file mode 100644
index 0000000..3302d31
--- /dev/null
+++ b/test/unit/https_test.rb
@@ -0,0 +1,11 @@
+# encoding: utf-8
+require 'test_helper'
+
+class HttpsTest < GeocoderTestCase
+
+  def test_uses_https_for_secure_query
+    Geocoder.configure(:use_https => true)
+    g = Geocoder::Lookup::Google.new
+    assert_match(/^https:/, g.query_url(Geocoder::Query.new("test")))
+  end
+end
diff --git a/test/unit/ip_address_test.rb b/test/unit/ip_address_test.rb
new file mode 100644
index 0000000..aa00672
--- /dev/null
+++ b/test/unit/ip_address_test.rb
@@ -0,0 +1,56 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpAddressTest < GeocoderTestCase
+
+  def test_valid
+    assert Geocoder::IpAddress.new("232.65.123.94").valid?
+    assert Geocoder::IpAddress.new(IPAddr.new("232.65.123.94")).valid?
+    assert !Geocoder::IpAddress.new("666.65.123.94").valid?
+    assert Geocoder::IpAddress.new("::ffff:12.34.56.78").valid?
+    assert Geocoder::IpAddress.new("3ffe:0b00:0000:0000:0001:0000:0000:000a").valid?
+    assert Geocoder::IpAddress.new(IPAddr.new("3ffe:0b00:0000:0000:0001:0000:0000:000a")).valid?
+    assert Geocoder::IpAddress.new("::1").valid?
+    assert !Geocoder::IpAddress.new("232.65.123.94.43").valid?
+    assert !Geocoder::IpAddress.new("232.65.123").valid?
+    assert !Geocoder::IpAddress.new("::ffff:123.456.789").valid?
+    assert !Geocoder::IpAddress.new("Test\n232.65.123.94").valid?
+    assert Geocoder::IpAddress.new("[3ffe:0b00:000:0000:0001:0000:0000:000a]:80").valid?
+  end
+
+  def test_internal
+    assert Geocoder::IpAddress.new("0.0.0.0").internal?
+    assert Geocoder::IpAddress.new("127.0.0.1").internal?
+    assert Geocoder::IpAddress.new("::1").internal?
+    assert Geocoder::IpAddress.new("172.19.0.1").internal?
+    assert Geocoder::IpAddress.new("10.100.100.1").internal?
+    assert Geocoder::IpAddress.new("192.168.0.1").internal?
+    assert !Geocoder::IpAddress.new("232.65.123.234").internal?
+    assert !Geocoder::IpAddress.new("127 Main St.").internal?
+    assert !Geocoder::IpAddress.new("John Doe\n127 Main St.\nAnywhere, USA").internal?
+  end
+
+  def test_loopback
+    assert Geocoder::IpAddress.new("0.0.0.0").loopback?
+    assert Geocoder::IpAddress.new("127.0.0.1").loopback?
+    assert Geocoder::IpAddress.new("::1").loopback?
+    assert !Geocoder::IpAddress.new("172.19.0.1").loopback?
+    assert !Geocoder::IpAddress.new("10.100.100.1").loopback?
+    assert !Geocoder::IpAddress.new("192.168.0.1").loopback?
+    assert !Geocoder::IpAddress.new("232.65.123.234").loopback?
+    assert !Geocoder::IpAddress.new("127 Main St.").loopback?
+    assert !Geocoder::IpAddress.new("John Doe\n127 Main St.\nAnywhere, USA").loopback?
+  end
+
+  def test_private
+    assert Geocoder::IpAddress.new("172.19.0.1").private?
+    assert Geocoder::IpAddress.new("10.100.100.1").private?
+    assert Geocoder::IpAddress.new("192.168.0.1").private?
+    assert !Geocoder::IpAddress.new("0.0.0.0").private?
+    assert !Geocoder::IpAddress.new("127.0.0.1").private?
+    assert !Geocoder::IpAddress.new("::1").private?
+    assert !Geocoder::IpAddress.new("232.65.123.234").private?
+    assert !Geocoder::IpAddress.new("127 Main St.").private?
+    assert !Geocoder::IpAddress.new("John Doe\n127 Main St.\nAnywhere, USA").private?
+  end
+end
diff --git a/test/unit/logger_test.rb b/test/unit/logger_test.rb
new file mode 100644
index 0000000..c018ee1
--- /dev/null
+++ b/test/unit/logger_test.rb
@@ -0,0 +1,67 @@
+# encoding: utf-8
+require 'test_helper'
+require 'logger'
+require 'tempfile'
+
+class LoggerTest < GeocoderTestCase
+
+  def setup
+    @tempfile = Tempfile.new("log")
+    @logger = Logger.new(@tempfile.path)
+    Geocoder.configure(logger: @logger)
+  end
+
+  def teardown
+    @logger.close
+    @tempfile.close
+  end
+
+  def test_set_logger_logs
+    assert_equal nil, Geocoder.log(:warn, "should log")
+    assert_match(/should log\n$/, @tempfile.read)
+  end
+
+  def test_logger_does_not_log_severity_too_low
+    @logger.level = Logger::ERROR
+    Geocoder.log(:info, "should not log")
+    assert_equal "", @tempfile.read
+  end
+
+  def test_logger_logs_when_severity_high_enough
+    @logger.level = Logger::DEBUG
+    Geocoder.log(:warn, "important: should log!")
+    assert_match(/important: should log/, @tempfile.read)
+  end
+
+  def test_kernel_logger_does_not_log_severity_too_low
+    assert_nothing_raised do
+      Geocoder.configure(logger: :kernel, kernel_logger_level: ::Logger::FATAL)
+      Geocoder.log(:info, "should not log")
+    end
+  end
+
+  def test_kernel_logger_logs_when_severity_high_enough
+    assert_raises RuntimeError do
+      Geocoder.configure(logger: :kernel, kernel_logger_level: ::Logger::DEBUG)
+      Geocoder.log(:error, "important: should log!")
+    end
+  end
+
+  def test_raise_configruation_error_for_invalid_logger
+    Geocoder.configure(logger: {})
+    assert_raises Geocoder::ConfigurationError do
+      Geocoder.log(:info, "should raise error")
+    end
+  end
+
+  def test_set_logger_always_returns_nil
+    assert_equal nil, Geocoder.log(:info, "should log")
+  end
+
+  def test_kernel_logger_always_returns_nil
+    Geocoder.configure(logger: :kernel)
+    silence_warnings do
+      assert_equal nil, Geocoder.log(:warn, "should log")
+    end
+  end
+end
diff --git a/test/unit/lookup_test.rb b/test/unit/lookup_test.rb
new file mode 100644
index 0000000..2728737
--- /dev/null
+++ b/test/unit/lookup_test.rb
@@ -0,0 +1,202 @@
+# encoding: utf-8
+require 'test_helper'
+
+class LookupTest < GeocoderTestCase
+  def test_responds_to_name_method
+    Geocoder::Lookup.all_services.each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      assert lookup.respond_to?(:name),
+        "Lookup #{l} does not respond to #name method."
+    end
+  end
+
+  def test_search_returns_empty_array_when_no_results
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      next if [
+        :abstract_api,
+        :ipgeolocation,
+        :ipqualityscore,
+        :melissa_street,
+        :nationaal_georegister_nl,
+        :twogis
+      ].include?(l) # lookups that always return a result
+
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      silence_warnings do
+        assert_equal [], lookup.send(:results, Geocoder::Query.new("no results")),
+          "Lookup #{l} does not return empty array when no results."
+      end
+    end
+  end
+
+  def test_query_url_contains_values_in_params_hash
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      next if [:freegeoip, :maxmind_local, :telize, :pointpin, :geoip2, :maxmind_geoip2, :mapbox, :ipdata_co, :ipinfo_io, :ipapi_com, :ipregistry, :ipstack, :postcodes_io, :uk_ordnance_survey_names, :amazon_location_service, :ipbase].include? l # does not use query string
+      set_api_key!(l)
+      url = Geocoder::Lookup.get(l).query_url(Geocoder::Query.new(
+        "test", :params => {:one_in_the_hand => "two in the bush"}
+      ))
+      assert_match(/one_in_the_hand=two\+in\+the\+bush/, url,
+        "Lookup #{l} does not appear to support arbitrary params in URL")
+    end
+  end
+
+  {
+    :esri => :l,
+    :bing => :key,
+    :geocoder_ca => :auth,
+    :google => :language,
+    :google_premier => :language,
+    :mapquest => :key,
+    :maxmind => :l,
+    :nominatim => :"accept-language",
+    :yandex => :lang
+  }.each do |l,p|
+    define_method "test_passing_param_to_#{l}_query_overrides_configuration_value" do
+      set_api_key!(l)
+      url = Geocoder::Lookup.get(l).query_url(Geocoder::Query.new(
+        "test", :params => {p => "xxxx"}
+      ))
+      assert_match(/#{p}=xxxx/, url,
+        "Param passed to #{l} lookup does not override configuration value")
+    end
+  end
+
+  {
+    :bing => :culture,
+    :google => :language,
+    :google_premier => :language,
+    :here => :lang,
+    :nominatim => :"accept-language",
+    :yandex => :lang
+  }.each do |l,p|
+    define_method "test_passing_language_to_#{l}_query_overrides_configuration_value" do
+      set_api_key!(l)
+      url = Geocoder::Lookup.get(l).query_url(Geocoder::Query.new(
+        "test", :language => 'xxxx'
+      ))
+      assert_match(/#{p}=xxxx/, url,
+        "Param passed to #{l} lookup does not override configuration value")
+    end
+  end
+
+  def test_raises_exception_on_invalid_key
+    Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    #Geocoder::Lookup.all_services_except_test.each do |l|
+    [:bing, :yandex, :maxmind, :baidu, :baidu_ip, :amap].each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      assert_raises Geocoder::InvalidApiKey do
+        lookup.send(:results, Geocoder::Query.new("invalid key"))
+      end
+    end
+  end
+
+  def test_returns_empty_array_on_invalid_key
+    silence_warnings do
+      #Geocoder::Lookup.all_services_except_test.each do |l|
+      [:bing, :yandex, :maxmind, :baidu, :baidu_ip, :amap].each do |l|
+        Geocoder.configure(:lookup => l)
+        set_api_key!(l)
+        assert_equal [], Geocoder.search("invalid key")
+      end
+    end
+  end
+
+  def test_does_not_choke_on_nil_address
+    Geocoder::Lookup.all_services.each do |l|
+      Geocoder.configure(:lookup => l)
+      assert_nothing_raised { Place.new("Place", nil).geocode }
+    end
+  end
+
+  def test_hash_to_query
+    g = Geocoder::Lookup::Google.new
+    assert_equal "a=1&b=2", g.send(:hash_to_query, {:a => 1, :b => 2})
+  end
+
+  def test_baidu_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::BaiduIp.new
+    assert_match "ak=MY_KEY", g.query_url(Geocoder::Query.new("232.65.123.94"))
+  end
+
+  def test_baidu_ip_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::Baidu.new
+    assert_match "ak=MY_KEY", g.query_url(Geocoder::Query.new("Madison Square Garden, New York, NY  10001, United States"))
+  end
+
+  def test_db_ip_com_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::DbIpCom.new
+    assert_match "\/MY_KEY\/", g.query_url(Geocoder::Query.new("232.65.123.94"))
+  end
+
+  def test_pointpin_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::Pointpin.new
+    assert_match "/MY_KEY/", g.query_url(Geocoder::Query.new("232.65.123.94"))
+  end
+
+  def test_google_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::Google.new
+    assert_match "key=MY_KEY", g.query_url(Geocoder::Query.new("Madison Square Garden, New York, NY  10001, United States"))
+  end
+
+  def test_geocoder_ca_showpostal
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::GeocoderCa.new
+    assert_match "showpostal=1", g.query_url(Geocoder::Query.new("Madison Square Garden, New York, NY  10001, United States"))
+  end
+
+  def test_telize_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::Telize.new
+    assert_match "rapidapi-key=MY_KEY", g.query_url(Geocoder::Query.new("232.65.123.94"))
+  end
+
+  def test_ipinfo_io_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::IpinfoIo.new
+    assert_match "token=MY_KEY", g.query_url(Geocoder::Query.new("232.65.123.94"))
+  end
+
+  def test_ipregistry_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::Ipregistry.new
+    assert_match "key=MY_KEY", g.query_url(Geocoder::Query.new("232.65.123.94"))
+  end
+
+  def test_amap_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::Amap.new
+    assert_match "key=MY_KEY", g.query_url(Geocoder::Query.new("202.198.16.3"))
+  end
+
+  def test_raises_configuration_error_on_missing_key
+    [:bing, :baidu, :amap].each do |l|
+      assert_raises Geocoder::ConfigurationError do
+        Geocoder.configure(:lookup => l, :api_key => nil)
+        Geocoder.search("Madison Square Garden, New York, NY  10001, United States")
+      end
+    end
+  end
+
+  def test_lookup_requires_lookup_file_when_class_name_shadowed_by_existing_constant
+    Geocoder::Lookup.street_services << :mock_lookup
+
+    assert_raises LoadError do
+      Geocoder.configure(:lookup => :mock_lookup, :api_key => "MY_KEY")
+      Geocoder.search("Madison Square Garden, New York, NY  10001, United States")
+    end
+
+    Geocoder::Lookup.street_services.reject! { |service| service == :mock_lookup }
+  end
+
+  def test_handle
+    assert_equal :google, Geocoder::Lookup::Google.new.handle
+    assert_equal :geocoder_ca, Geocoder::Lookup::GeocoderCa.new.handle
+  end
+end
diff --git a/test/unit/lookups/abstract_api_test.rb b/test/unit/lookups/abstract_api_test.rb
new file mode 100644
index 0000000..f165f62
--- /dev/null
+++ b/test/unit/lookups/abstract_api_test.rb
@@ -0,0 +1,25 @@
+# encoding: utf-8
+require 'test_helper'
+
+class AbstractApiTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :abstract_api)
+    set_api_key!(:abstract_api)
+  end
+
+  def test_result_attributes
+    result = Geocoder.search('2.19.128.50').first
+    assert_equal 'Seattle, WA 98111, United States', result.address
+    assert_equal 'Seattle', result.city
+    assert_equal 'WA', result.state_code
+    assert_equal 'Washington', result.state
+    assert_equal 'United States', result.country
+    assert_equal 'US', result.country_code
+    assert_equal '98111', result.postal_code
+    assert_equal 47.6032, result.latitude
+    assert_equal(-122.3412, result.longitude)
+    assert_equal [47.6032, -122.3412], result.coordinates
+  end
+end
diff --git a/test/unit/lookups/amazon_location_service_test.rb b/test/unit/lookups/amazon_location_service_test.rb
new file mode 100644
index 0000000..faca884
--- /dev/null
+++ b/test/unit/lookups/amazon_location_service_test.rb
@@ -0,0 +1,24 @@
+# encoding: utf-8
+require 'test_helper'
+
+class AmazonLocationServiceTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :amazon_location_service, amazon_location_service: {index_name: "some_index_name"})
+  end
+
+  def test_amazon_location_service_geocoding
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Madison Ave, Staten Island, NY, 10314, USA", result.address
+    assert_equal "Staten Island", result.city
+    assert_equal "New York", result.state
+  end
+
+  def test_amazon_location_service_reverse_geocoding
+    result = Geocoder.search([45.423733, -75.676333]).first
+    assert_equal "Madison Ave, Staten Island, NY, 10314, USA", result.address
+    assert_equal "Staten Island", result.city
+    assert_equal "New York", result.state
+  end
+end
diff --git a/test/unit/lookups/ban_data_gouv_fr_test.rb b/test/unit/lookups/ban_data_gouv_fr_test.rb
new file mode 100644
index 0000000..40ed8ff
--- /dev/null
+++ b/test/unit/lookups/ban_data_gouv_fr_test.rb
@@ -0,0 +1,147 @@
+# encoding: utf-8
+require 'test_helper'
+
+class BanDataGouvFrTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :ban_data_gouv_fr, use_https: true)
+  end
+
+  def test_query_for_geocode
+    query = Geocoder::Query.new('13 rue yves toudic, 75010 Paris')
+    lookup = Geocoder::Lookup.get(:ban_data_gouv_fr)
+    res = lookup.query_url(query)
+    assert_equal 'https://api-adresse.data.gouv.fr/search/?q=13+rue+yves+toudic%2C+75010+Paris', res
+  end
+
+  def test_query_for_geocode_with_geographic_priority
+    query = Geocoder::Query.new('13 rue yves toudic, 75010 Paris', lat: 48.789, lon: 2.789)
+    lookup = Geocoder::Lookup.get(:ban_data_gouv_fr)
+    res = lookup.query_url(query)
+    assert_equal 'https://api-adresse.data.gouv.fr/search/?lat=48.789&lon=2.789&q=13+rue+yves+toudic%2C+75010+Paris', res
+  end
+
+  def test_query_for_reverse_geocode
+    query = Geocoder::Query.new([48.770639, 2.364375])
+    lookup = Geocoder::Lookup.get(:ban_data_gouv_fr)
+    res = lookup.query_url(query)
+    assert_equal 'https://api-adresse.data.gouv.fr/reverse/?lat=48.770639&lon=2.364375', res
+  end
+
+  def test_results_component
+    result = Geocoder.search('13 rue yves toudic, 75010 Paris').first
+    assert_equal 'ADRNIVX_0000000270748760', result.location_id
+    assert_equal 'housenumber', result.result_type
+    assert_equal 'Paris', result.city_name
+    assert_equal '13 Rue Yves Toudic 75010 Paris, France', result.international_address
+    assert_equal '13 Rue Yves Toudic 75010 Paris, France', result.address
+    assert_equal '13 Rue Yves Toudic 75010 Paris', result.national_address
+    assert_equal '13 Rue Yves Toudic', result.street_address
+    assert_equal '13', result.street_number
+    assert_equal 'Rue Yves Toudic', result.street
+    assert_equal 'Rue Yves Toudic', result.street_name
+    assert_equal 'Paris', result.city
+    assert_equal 'Paris', result.city_name
+    assert_equal '75110', result.city_code
+    assert_equal '75010', result.postal_code
+    assert_equal '75', result.department_code
+    assert_equal 'Paris', result.department_name
+    assert_equal 'Île-de-France', result.region_name
+    assert_equal '11', result.region_code
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(48.870131, result.coordinates[0])
+    assert_equal(2.363473, result.coordinates[1])
+  end
+
+  def test_paris_special_business_logic
+    result = Geocoder.search('paris').first
+    assert_equal 'city', result.result_type
+    assert_equal '75000', result.postal_code
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(2244000, result.population)
+    assert_equal 'Paris', result.city
+    assert_equal 'Paris', result.city_name
+    assert_equal '75056', result.city_code
+    assert_equal '75000', result.postal_code
+    assert_equal '75', result.department_code
+    assert_equal 'Paris', result.department_name
+    assert_equal 'Île-de-France', result.region_name
+    assert_equal '11', result.region_code
+    assert_equal(48.8589, result.coordinates[0])
+    assert_equal(2.3469, result.coordinates[1])
+  end
+
+  def test_city_result_methods
+    result = Geocoder.search('montpellier').first
+    assert_equal 'city', result.result_type
+    assert_equal '34080', result.postal_code
+    assert_equal '34172', result.city_code
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(5, result.administrative_weight)
+    assert_equal(255100, result.population)
+    assert_equal '34', result.department_code
+    assert_equal 'Hérault', result.department_name
+    assert_equal 'Occitanie', result.region_name
+    assert_equal '76', result.region_code
+    assert_equal(43.611024, result.coordinates[0])
+    assert_equal(3.875521, result.coordinates[1])
+  end
+
+  def test_results_component_when_reverse_geocoding
+    result = Geocoder.search([48.770431, 2.364463]).first
+    assert_equal '94021_1133_49638b', result.location_id
+    assert_equal 'housenumber', result.result_type
+    assert_equal '4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue, France', result.international_address
+    assert_equal '4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue, France', result.address
+    assert_equal '4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue', result.national_address
+    assert_equal '4 Rue du Lieutenant Alain le Coz', result.street_address
+    assert_equal '4', result.street_number
+    assert_equal 'Rue du Lieutenant Alain le Coz', result.street
+    assert_equal 'Rue du Lieutenant Alain le Coz', result.street_name
+    assert_equal 'Chevilly-Larue', result.city
+    assert_equal 'Chevilly-Larue', result.city_name
+    assert_equal '94021', result.city_code
+    assert_equal '94550', result.postal_code
+    assert_equal '94', result.department_code
+    assert_equal 'Val-de-Marne', result.department_name
+    assert_equal 'Île-de-France', result.region_name
+    assert_equal '11', result.region_code
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(48.770639, result.coordinates[0])
+    assert_equal(2.364375, result.coordinates[1])
+  end
+
+  def test_no_reverse_results
+    result = Geocoder.search('no reverse results')
+    assert_equal 0, result.length
+  end
+
+  def test_actual_make_api_request_with_https
+    Geocoder.configure(use_https: true, lookup: :ban_data_gouv_fr)
+
+    require 'webmock/test_unit'
+    WebMock.enable!
+    stub_all = WebMock.stub_request(:any, /.*/).to_return(status: 200)
+
+    g = Geocoder::Lookup::BanDataGouvFr.new
+    g.send(:actual_make_api_request, Geocoder::Query.new('test location'))
+    assert_requested(stub_all)
+
+    WebMock.reset!
+    WebMock.disable!
+  end
+
+
+  private
+
+  def assert_country_code(result)
+    [:state_code, :country_code, :province_code].each do |method|
+      assert_equal 'FR', result.send(method)
+    end
+  end
+end
diff --git a/test/unit/lookups/bing_test.rb b/test/unit/lookups/bing_test.rb
new file mode 100644
index 0000000..e3d1b22
--- /dev/null
+++ b/test/unit/lookups/bing_test.rb
@@ -0,0 +1,96 @@
+# encoding: utf-8
+require 'test_helper'
+
+class BingTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :bing)
+    set_api_key!(:bing)
+  end
+
+  def test_query_for_reverse_geocode
+    lookup = Geocoder::Lookup::Bing.new
+    url = lookup.query_url(Geocoder::Query.new([45.423733, -75.676333]))
+    assert_match(/Locations\/45.423733/, url)
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Madison Square Garden, NY", result.address
+    assert_equal "NY", result.state
+    assert_equal "New York", result.city
+  end
+
+  def test_result_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [
+      40.744944289326668,
+      -74.002353921532631,
+      40.755675807595253,
+      -73.983625397086143
+    ], result.viewport
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+  def test_query_url_contains_region
+    lookup = Geocoder::Lookup::Bing.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "manchester",
+      :region => "uk"
+    ))
+    assert_match(%r!Locations/uk/\?q=manchester!, url)
+  end
+
+  def test_query_url_without_region
+    lookup = Geocoder::Lookup::Bing.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "manchester"
+    ))
+    assert_match(%r!Locations/\?q=manchester!, url)
+  end
+
+  def test_query_url_escapes_spaces_in_address
+    lookup = Geocoder::Lookup::Bing.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "manchester, lancashire",
+      :region => "uk"
+    ))
+    assert_match(%r!Locations/uk/\?q=manchester%2C\+lancashire!, url)
+  end
+
+  def test_query_url_strips_trailing_and_leading_spaces
+    lookup = Geocoder::Lookup::Bing.new
+    url = lookup.query_url(Geocoder::Query.new(
+      " manchester, lancashire ",
+      :region => "uk"
+    ))
+    assert_match(%r!Locations/uk/\?q=manchester%2C\+lancashire!, url)
+  end
+
+  def test_raises_exception_when_service_unavailable
+    Geocoder.configure(:always_raise => [Geocoder::ServiceUnavailable])
+    l = Geocoder::Lookup.get(:bing)
+    assert_raises Geocoder::ServiceUnavailable do
+      l.send(:results, Geocoder::Query.new("service unavailable"))
+    end
+  end
+
+  def test_raises_exception_when_bing_returns_forbidden_request
+    Geocoder.configure(:always_raise => [Geocoder::RequestDenied])
+    assert_raises Geocoder::RequestDenied do
+      Geocoder.search("forbidden request")
+    end
+  end
+
+  def test_raises_exception_when_bing_returns_internal_server_error
+    Geocoder.configure(:always_raise => [Geocoder::ServiceUnavailable])
+    assert_raises Geocoder::ServiceUnavailable do
+      Geocoder.search("internal server error")
+    end
+  end
+end
diff --git a/test/unit/lookups/db_ip_com_test.rb b/test/unit/lookups/db_ip_com_test.rb
new file mode 100644
index 0000000..bef1cb6
--- /dev/null
+++ b/test/unit/lookups/db_ip_com_test.rb
@@ -0,0 +1,72 @@
+require 'test_helper'
+
+class DbIpComTest < GeocoderTestCase
+  def configure_for_free_api_access
+    Geocoder.configure(ip_lookup: :db_ip_com, db_ip_com: { api_key: 'MY_API_KEY' })
+    set_api_key!(:db_ip_com)
+  end
+
+  def configure_for_paid_api_access
+    Geocoder.configure(ip_lookup: :db_ip_com, db_ip_com: { api_key: 'MY_API_KEY', use_https: true })
+    set_api_key!(:db_ip_com)
+  end
+
+  def test_no_results
+    configure_for_free_api_access
+    results = Geocoder.search('no results')
+    assert_equal 0, results.length
+  end
+
+  def test_result_on_ip_address_search
+    configure_for_free_api_access
+    result = Geocoder.search('23.255.240.0').first
+    assert result.is_a?(Geocoder::Result::DbIpCom)
+  end
+
+  def test_result_components
+    configure_for_free_api_access
+    result = Geocoder.search('23.255.240.0').first
+
+    assert_equal [37.3861, -122.084], result.coordinates
+    assert_equal 'Mountain View, CA 94043, United States', result.address
+    assert_equal 'Mountain View', result.city
+    assert_equal 'Santa Clara County', result.district
+    assert_equal 'CA', result.state_code
+    assert_equal '94043', result.zip_code
+    assert_equal 'United States', result.country_name
+    assert_equal 'US', result.country_code
+    assert_equal 'North America', result.continent_name
+    assert_equal 'NA', result.continent_code
+    assert_equal 'America/Los_Angeles', result.time_zone
+    assert_equal(-7, result.gmt_offset)
+    assert_equal 'USD', result.currency_code
+  end
+
+  def test_free_host_config
+    configure_for_free_api_access
+    lookup = Geocoder::Lookup::DbIpCom.new
+    query = Geocoder::Query.new('23.255.240.0')
+    assert_match 'http://api.db-ip.com/v2/MY_API_KEY/23.255.240.0', lookup.query_url(query)
+  end
+
+  def test_paid_host_config
+    configure_for_paid_api_access
+    lookup = Geocoder::Lookup::DbIpCom.new
+    query = Geocoder::Query.new('23.255.240.0')
+    assert_match 'https://api.db-ip.com/v2/MY_API_KEY/23.255.240.0', lookup.query_url(query)
+  end
+
+  def test_raises_over_limit_exception
+    Geocoder.configure always_raise: :all
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder::Lookup::DbIpCom.new.send(:results, Geocoder::Query.new('quota exceeded'))
+    end
+  end
+
+  def test_raises_unknown_error
+    Geocoder.configure always_raise: :all
+    assert_raises Geocoder::Error do
+      Geocoder::Lookup::DbIpCom.new.send(:results, Geocoder::Query.new('unknown error'))
+    end
+  end
+end
diff --git a/test/unit/lookups/dstk_test.rb b/test/unit/lookups/dstk_test.rb
new file mode 100644
index 0000000..8783ab9
--- /dev/null
+++ b/test/unit/lookups/dstk_test.rb
@@ -0,0 +1,26 @@
+# encoding: utf-8
+require 'test_helper'
+
+class DstkTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :dstk)
+  end
+
+  def test_dstk_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Manhattan", result.address_components_of_type(:sublocality).first['long_name']
+  end
+
+  def test_dstk_query_url
+    query = Geocoder::Query.new("Madison Square Garden, New York, NY")
+    assert_equal "http://www.datasciencetoolkit.org/maps/api/geocode/json?address=Madison+Square+Garden%2C+New+York%2C+NY&language=en&sensor=false", query.url
+  end
+
+  def test_dstk_query_url_with_custom_host
+    Geocoder.configure(dstk: {host: 'NOT_AN_ACTUAL_HOST'})
+    query = Geocoder::Query.new("Madison Square Garden, New York, NY")
+    assert_equal "http://NOT_AN_ACTUAL_HOST/maps/api/geocode/json?address=Madison+Square+Garden%2C+New+York%2C+NY&language=en&sensor=false", query.url
+  end
+end
diff --git a/test/unit/lookups/esri_test.rb b/test/unit/lookups/esri_test.rb
new file mode 100644
index 0000000..dfb3fa7
--- /dev/null
+++ b/test/unit/lookups/esri_test.rb
@@ -0,0 +1,202 @@
+# encoding: utf-8
+require 'test_helper'
+require 'geocoder/esri_token'
+
+class EsriTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :esri)
+  end
+
+  def test_query_for_geocode
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+    res = lookup.query_url(query)
+    assert_equal "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find?f=pjson&outFields=%2A&text=Bluffton%2C+SC",
+      res
+  end
+
+  def test_query_for_geocode_with_source_country
+    Geocoder.configure(esri: {source_country: 'USA'})
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{sourceCountry=USA}, url
+  end
+
+  def test_query_for_geocode_with_preferred_label_values
+    Geocoder.configure(esri: {preferred_label_values: 'localCity'})
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{preferredLabelValues=localCity}, url
+  end
+
+  def test_query_for_geocode_with_token_and_for_storage
+    token = Geocoder::EsriToken.new('xxxxx', Time.now + 60*60*24)
+    Geocoder.configure(esri: {token: token, for_storage: true})
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{forStorage=true}, url
+    assert_match %r{token=xxxxx}, url
+  end
+
+  def test_token_from_options
+    options_token = Geocoder::EsriToken.new('options_token', Time.now + 60*60*24)
+    query = Geocoder::Query.new("Bluffton, SC", token: options_token)
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{token=options_token}, url
+  end
+
+  def test_token_from_options_overrides_configuration
+    config_token = Geocoder::EsriToken.new('config_token', Time.now + 60*60*24)
+    options_token = Geocoder::EsriToken.new('options_token', Time.now + 60*60*24)
+    Geocoder.configure(esri: { token: config_token })
+    query = Geocoder::Query.new("Bluffton, SC", token: options_token)
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{token=options_token}, url
+  end
+
+  def test_query_for_geocode_with_config_for_storage_false
+    Geocoder.configure(esri: {for_storage: false})
+
+    query = Geocoder::Query.new("Bluffton, SC", for_storage: true)
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{forStorage=true}, url
+
+    query = Geocoder::Query.new("Bluffton, SC", for_storage: false)
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_no_match %r{forStorage}, url
+  end
+
+  def test_query_for_geocode_with_config_for_storage_true
+    Geocoder.configure(esri: {for_storage: true})
+
+    query = Geocoder::Query.new("Bluffton, SC", for_storage: true)
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_match %r{forStorage=true}, url
+
+    query = Geocoder::Query.new("Bluffton, SC", for_storage: false)
+    lookup = Geocoder::Lookup.get(:esri)
+    url = lookup.query_url(query)
+    assert_no_match %r{forStorage}, url
+  end
+
+  def test_token_generation_doesnt_overwrite_existing_config
+    Geocoder.configure(esri: {api_key: ['id','secret'], for_storage: true})
+
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+
+    lookup.instance_eval do
+      # redefine `create_token` to return a manually-created token
+      def create_token
+        "xxxxx"
+      end
+    end
+
+    url = lookup.query_url(query)
+
+    assert_match %r{forStorage=true}, url
+    assert_match %r{token=xxxxx}, url
+  end
+
+  def test_query_for_reverse_geocode
+    query = Geocoder::Query.new([45.423733, -75.676333])
+    lookup = Geocoder::Lookup.get(:esri)
+    res = lookup.query_url(query)
+    assert_equal "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode?f=pjson&location=-75.676333%2C45.423733&outFields=%2A",
+      res
+  end
+
+  def test_results_component
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "10001", result.postal_code
+    assert_equal "USA", result.country
+    assert_equal "Madison Square Garden", result.address
+    assert_equal "New York", result.city
+    assert_equal "New York", result.state
+    assert_equal "NY", result.state_code
+    assert_equal "Madison Square Garden", result.place_name
+    assert_equal "Sports Complex", result.place_type
+    assert_equal(40.75004981300049, result.coordinates[0])
+    assert_equal(-73.99423889799965, result.coordinates[1])
+  end
+
+  def test_results_component_when_location_type_is_national_capital
+    result = Geocoder.search("washington dc").first
+    assert_equal "Washington", result.city
+    assert_equal "District of Columbia", result.state
+    assert_equal "DC", result.state_code
+    assert_equal "USA", result.country
+    assert_equal "Washington, D. C., District of Columbia, United States", result.address
+    assert_equal "Washington", result.place_name
+    assert_equal "National Capital", result.place_type
+    assert_equal(38.895107833000452, result.coordinates[0])
+    assert_equal(-77.036365517999627, result.coordinates[1])
+  end
+
+  def test_results_component_when_location_type_is_state_capital
+    result = Geocoder.search("austin tx").first
+    assert_equal "Austin", result.city
+    assert_equal "Texas", result.state
+    assert_equal "TX", result.state_code
+    assert_equal "USA", result.country
+    assert_equal "Austin, Texas, United States", result.address
+    assert_equal "Austin", result.place_name
+    assert_equal "State Capital", result.place_type
+    assert_equal(30.267145960000448, result.coordinates[0])
+    assert_equal(-97.743055550999657, result.coordinates[1])
+  end
+
+  def test_results_component_when_location_type_is_city
+    result = Geocoder.search("new york ny").first
+    assert_equal "New York City", result.city
+    assert_equal "New York", result.state
+    assert_equal "NY", result.state_code
+    assert_equal "USA", result.country
+    assert_equal "New York City, New York, United States", result.address
+    assert_equal "New York City", result.place_name
+    assert_equal "City", result.place_type
+    assert_equal(40.714269404000447, result.coordinates[0])
+    assert_equal(-74.005969928999662, result.coordinates[1])
+  end
+
+  def test_results_component_when_reverse_geocoding
+    result = Geocoder.search([45.423733, -75.676333]).first
+    assert_equal "75007", result.postal_code
+    assert_equal "FRA", result.country
+    assert_equal "4 Avenue Gustave Eiffel", result.address
+    assert_equal "Paris", result.city
+    assert_equal "Île-de-France", result.state
+    assert_equal "Île-de-France", result.state_code
+    assert_equal "4 Avenue Gustave Eiffel", result.place_name
+    assert_equal "Address", result.place_type
+    assert_equal(48.858129997357558, result.coordinates[0])
+    assert_equal(2.2956200048981574, result.coordinates[1])
+  end
+
+  def test_results_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.744050000000001, -74.000241000000003, 40.756050000000002, -73.988241000000002],
+      result.viewport
+  end
+
+  def test_cache_key_doesnt_include_api_key_or_token
+    token = Geocoder::EsriToken.new('xxxxx', Time.now + 60)
+    Geocoder.configure(esri: {token: token, api_key: "xxxxx", for_storage: true})
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+    key = lookup.send(:cache_key, query)
+    assert_match %r{forStorage}, key
+    assert_no_match %r{token}, key
+    assert_no_match %r{api_key}, key
+  end
+end
diff --git a/test/unit/lookups/freegeoip_test.rb b/test/unit/lookups/freegeoip_test.rb
new file mode 100644
index 0000000..ed2ea99
--- /dev/null
+++ b/test/unit/lookups/freegeoip_test.rb
@@ -0,0 +1,41 @@
+# encoding: utf-8
+require 'test_helper'
+
+class FreegeoipTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :freegeoip)
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::Freegeoip)
+  end
+
+  def test_result_on_loopback_ip_address_search
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal 'RD',        result.country_code
+    assert_equal "Reserved",  result.country
+  end
+
+  def test_result_on_private_ip_address_search
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "172.19.0.1", result.ip
+    assert_equal 'RD',         result.country_code
+    assert_equal "Reserved",   result.country
+  end
+
+  def test_result_components
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Plano, TX 75093, United States", result.address
+  end
+
+  def test_host_config
+    Geocoder.configure(freegeoip: {host: "local.com"})
+    lookup = Geocoder::Lookup::Freegeoip.new
+    query = Geocoder::Query.new("24.24.24.23")
+    assert_match %r(https://local\.com), lookup.query_url(query)
+  end
+end
diff --git a/test/unit/lookups/geoapify_test.rb b/test/unit/lookups/geoapify_test.rb
new file mode 100644
index 0000000..c256390
--- /dev/null
+++ b/test/unit/lookups/geoapify_test.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class GeoapifyTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :geoapify)
+    set_api_key!(:geoapify)
+  end
+
+  def test_geoapify_forward_geocoding_result_properties
+    result = Geocoder.search('Madison Square Garden, New York, NY').first
+
+    geometry = { type: 'Point', coordinates: [-73.993368, 40.750487] }
+    bounds = [-73.9944446, 40.7498531, -73.9925924, 40.751161]
+    rank = { popularity: 8.615793062435909, confidence: 1, match_type: :full_match }
+    datasource = {
+      sourcename: 'openstreetmap',
+      wheelchair: 'limited',
+      wikidata: 'Q186125',
+      wikipedia: 'en:Madison Square Garden',
+      website: 'http://www.thegarden.com/',
+      phone: '12124656741',
+      osm_type: 'W',
+      osm_id: 138_141_251,
+      continent: 'North America'
+    }
+
+    assert_equal(40.750487, result.latitude)
+    assert_equal(-73.993368, result.longitude)
+    assert_equal '4 Pennsylvania Plaza', result.address_line1
+    assert_equal 'New York, NY 10001, United States of America', result.address_line2
+    assert_equal '4', result.house_number
+    assert_equal 'Pennsylvania Plaza', result.street
+    assert_equal 'Manhattan', result.district
+    assert_equal 'Chelsea', result.suburb
+    assert_equal 'New York County', result.county
+    assert_equal geometry, result.geometry
+    assert_equal bounds, result.bounds
+    assert_equal :building, result.type
+    assert_nil result.distance # Only for reverse geocoding requests
+    assert_equal rank, result.rank
+    assert_equal datasource, result.datasource
+  end
+
+  def test_geoapify_reverse_geocoding_result_properties
+    result = Geocoder.search([45.423733, -75.676333]).first
+
+    geometry = { type: 'Point', coordinates: [-73.993368, 40.750487] }
+    bounds = [-73.9944446, 40.7498531, -73.9925924, 40.751161]
+    rank = { popularity: 8.615793062435909 }
+    datasource = {
+      sourcename: 'openstreetmap',
+      wheelchair: 'limited',
+      wikidata: 'Q186125',
+      wikipedia: 'en:Madison Square Garden',
+      website: 'http://www.thegarden.com/',
+      phone: '12124656741',
+      osm_type: 'W',
+      osm_id: 138_141_251,
+      continent: 'North America'
+    }
+
+    assert_equal(40.750487, result.latitude)
+    assert_equal(-73.993368, result.longitude)
+    assert_equal 'Madison Square Garden', result.address_line1
+    assert_equal '4 Pennsylvania Plaza, New York, NY 10001, United States of America', result.address_line2
+    assert_equal '4', result.house_number
+    assert_equal 'Pennsylvania Plaza', result.street
+    assert_equal 'Manhattan', result.district
+    assert_equal 'Chelsea', result.suburb
+    assert_equal 'New York County', result.county
+    assert_equal geometry, result.geometry
+    assert_equal bounds, result.bounds
+    assert_equal :amenity, result.type
+    assert_equal 14.791104652930729, result.distance
+    assert_equal rank, result.rank
+    assert_equal datasource, result.datasource
+  end
+
+  def test_geoapify_query_url_contains_language
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        language: 'de'
+      )
+    )
+    assert_match(/lang=de/, url)
+  end
+
+  def test_geoapify_query_url_contains_limit
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        limit: 5
+      )
+    )
+    assert_match(/limit=5/, url)
+  end
+
+  def test_geoapify_query_url_contains_api_key
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query'
+      )
+    )
+    assert_match(/apiKey=a+/, url)
+  end
+
+  def test_geoapify_query_url_contains_text
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query'
+      )
+    )
+    assert_match(/text=Test\+Query/, url)
+  end
+
+  def test_geoapify_query_url_contains_params
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        params: {
+          type: 'amenity',
+          filter: 'countrycode:us',
+          bias: 'countrycode:us'
+        }
+      )
+    )
+    assert_match(/bias=countrycode%3Aus/, url)
+    assert_match(/filter=countrycode%3Aus/, url)
+    assert_match(/type=amenity/, url)
+  end
+
+  def test_geoapify_reverse_query_url_contains_lat_lon
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        [45.423733, -75.676333]
+      )
+    )
+    assert_match(/lat=45\.423733/, url)
+    assert_match(/lon=-75\.676333/, url)
+  end
+
+  def test_geoapify_query_url_contains_autocomplete
+    lookup = Geocoder::Lookup::Geoapify.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        autocomplete: true
+      )
+    )
+    assert_match(/\/geocode\/autocomplete/, url)
+  end
+
+  def test_geoapify_invalid_request
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search('invalid request')
+    end
+  end
+
+  def test_geoapify_invalid_key
+    Geocoder.configure(always_raise: [Geocoder::RequestDenied])
+    assert_raises Geocoder::RequestDenied do
+      Geocoder.search('invalid key')
+    end
+  end
+end
diff --git a/test/unit/lookups/geocoder_ca_test.rb b/test/unit/lookups/geocoder_ca_test.rb
new file mode 100644
index 0000000..1d162e8
--- /dev/null
+++ b/test/unit/lookups/geocoder_ca_test.rb
@@ -0,0 +1,17 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GeocoderCaTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :geocoder_ca)
+    set_api_key!(:geocoder_ca)
+  end
+
+  def test_result_components
+    result = Geocoder.search([45.423733, -75.676333]).first
+    assert_equal "CA", result.country_code
+    assert_equal "289 Somerset ST E, Ottawa, ON K1N6W1, Canada", result.address
+  end
+end
diff --git a/test/unit/lookups/geocodio_test.rb b/test/unit/lookups/geocodio_test.rb
new file mode 100644
index 0000000..b8369f4
--- /dev/null
+++ b/test/unit/lookups/geocodio_test.rb
@@ -0,0 +1,70 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GeocodioTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :geocodio)
+    set_api_key!(:geocodio)
+  end
+
+  def test_result_components
+    result = Geocoder.search("1101 Pennsylvania Ave NW, Washington DC").first
+    assert_equal 1.0, result.accuracy
+    assert_equal "1101", result.number
+    assert_equal "1101 Pennsylvania Ave NW", result.street_address
+    assert_equal "Ave", result.suffix
+    assert_equal "DC", result.state
+    assert_equal "20004", result.zip
+    assert_equal "NW", result.postdirectional
+    assert_equal "Washington", result.city
+    assert_equal "US", result.country_code
+    assert_equal "United States", result.country
+    assert_equal "1101 Pennsylvania Ave NW, Washington, DC 20004", result.formatted_address
+    assert_equal({ "lat" => 38.895343, "lng" => -77.027385 }, result.location)
+  end
+
+  def test_reverse_canada_result
+    result = Geocoder.search([43.652961, -79.382624]).first
+    assert_equal 1.0, result.accuracy
+    assert_equal "483", result.number
+    assert_equal "Bay", result.street
+    assert_equal "St", result.suffix
+    assert_equal "ON", result.state
+    assert_equal "Toronto", result.city
+    assert_equal "CA", result.country_code
+    assert_equal "Canada", result.country
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+  def test_geocodio_reverse_url
+    query = Geocoder::Query.new([45.423733, -75.676333])
+    assert_match(/reverse/, query.url)
+  end
+
+  def test_raises_invalid_request_exception
+    Geocoder.configure(:always_raise => [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search("invalid")
+    end
+  end
+
+  def test_raises_api_key_exception
+    Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("bad api key")
+    end
+  end
+
+  def test_raises_over_limit_exception
+    Geocoder.configure(:always_raise => [Geocoder::OverQueryLimitError])
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search("over query limit")
+    end
+  end
+end
diff --git a/test/unit/lookups/geoip2_test.rb b/test/unit/lookups/geoip2_test.rb
new file mode 100644
index 0000000..b8e0fbe
--- /dev/null
+++ b/test/unit/lookups/geoip2_test.rb
@@ -0,0 +1,54 @@
+# encoding: utf-8
+require 'test_helper'
+
+class Geoip2Test < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :geoip2, file: 'test_file')
+  end
+
+  def test_result_attributes
+    result = Geocoder.search('8.8.8.8').first
+    assert_equal 'Mountain View, CA 94043, United States', result.address
+    assert_equal 'Mountain View', result.city
+    assert_equal 'CA', result.state_code
+    assert_equal 'California', result.state
+    assert_equal 'United States', result.country
+    assert_equal 'US', result.country_code
+    assert_equal '94043', result.postal_code
+    assert_equal 37.41919999999999, result.latitude
+    assert_equal(-122.0574, result.longitude)
+    assert_equal [37.41919999999999, -122.0574], result.coordinates
+  end
+
+  def test_loopback
+    results = Geocoder.search('127.0.0.1')
+    assert_equal [], results
+  end
+
+  def test_localization
+    result = Geocoder.search('8.8.8.8').first
+
+    Geocoder::Configuration.language = :ru
+    assert_equal 'Маунтин-Вью', result.city
+  end
+
+  def test_dynamic_localization
+    result = Geocoder.search('8.8.8.8').first
+
+    result.language = :ru
+
+    assert_equal 'Маунтин-Вью', result.city
+  end
+
+  def test_dynamic_localization_fallback
+    result = Geocoder.search('8.8.8.8').first
+
+    result.language = :unsupported_language
+
+    assert_equal 'Mountain View', result.city
+    assert_equal 'California', result.state
+    assert_equal 'United States', result.country
+  end
+end
diff --git a/test/unit/lookups/geoportail_lu_test.rb b/test/unit/lookups/geoportail_lu_test.rb
new file mode 100644
index 0000000..3ea695c
--- /dev/null
+++ b/test/unit/lookups/geoportail_lu_test.rb
@@ -0,0 +1,67 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GeoportailLuTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :geoportail_lu)
+  end
+
+  def test_query_for_geocode
+    query = Geocoder::Query.new('55 route de luxembourg, pontpierre')
+    lookup = Geocoder::Lookup.get(:geoportail_lu)
+    res = lookup.query_url(query)
+    assert_equal 'http://api.geoportail.lu/geocoder/search?queryString=55+route+de+luxembourg%2C+pontpierre', res
+  end
+
+  def test_query_for_reverse_geocode
+    query = Geocoder::Query.new([45.423733, -75.676333])
+    lookup = Geocoder::Lookup.get(:geoportail_lu)
+    res = lookup.query_url(query)
+    assert_equal 'http://api.geoportail.lu/geocoder/reverseGeocode?lat=45.423733&lon=-75.676333', res
+  end
+
+  def test_results_component
+    result = Geocoder.search('2 boulevard Royal, luxembourg').first
+    assert_equal '2449', result.postal_code
+    assert_equal 'Luxembourg', result.country
+    assert_equal '2 Boulevard Royal,2449 Luxembourg', result.address
+    assert_equal '2', result.street_number
+    assert_equal 'Boulevard Royal', result.street
+    assert_equal '2 Boulevard Royal', result.street_address
+    assert_equal 'Luxembourg', result.city
+    assert_equal 'Luxembourg', result.state
+    assert_country_code result
+    assert_equal(49.6146720749933, result.coordinates[0])
+    assert_equal(6.12939750216249, result.coordinates[1])
+  end
+
+  def test_results_component_when_reverse_geocoding
+    result = Geocoder.search([6.12476867352074, 49.6098566608772]).first
+    assert_equal '1724', result.postal_code
+    assert_equal 'Luxembourg', result.country
+    assert_equal '39 Boulevard Prince Henri,1724 Luxembourg', result.address
+    assert_equal '39', result.street_number
+    assert_equal 'Boulevard Prince Henri', result.street
+    assert_equal '39 Boulevard Prince Henri', result.street_address
+    assert_equal 'Luxembourg', result.city
+    assert_equal 'Luxembourg', result.state
+    assert_country_code result
+    assert_equal(49.6098566608772, result.coordinates[0])
+    assert_equal(6.12476867352074, result.coordinates[1])
+  end
+
+  def test_no_results
+    results = Geocoder.search('')
+    assert_equal 0, results.length
+  end
+
+  private
+
+  def assert_country_code(result)
+    [:state_code, :country_code, :province_code].each do |method|
+      assert_equal 'LU', result.send(method)
+    end
+  end
+end
diff --git a/test/unit/lookups/google_places_details_test.rb b/test/unit/lookups/google_places_details_test.rb
new file mode 100644
index 0000000..d0807f7
--- /dev/null
+++ b/test/unit/lookups/google_places_details_test.rb
@@ -0,0 +1,157 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GooglePlacesDetailsTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :google_places_details)
+    set_api_key!(:google_places_details)
+  end
+
+  def test_google_places_details_result_components
+    assert_equal "Manhattan", madison_square_garden.address_components_of_type(:sublocality).first["long_name"]
+  end
+
+  def test_google_places_details_result_components_contains_route
+    assert_equal "Pennsylvania Plaza", madison_square_garden.address_components_of_type(:route).first["long_name"]
+  end
+
+  def test_google_places_details_result_components_contains_street_number
+    assert_equal "4", madison_square_garden.address_components_of_type(:street_number).first["long_name"]
+  end
+
+  def test_google_places_details_street_address_returns_formatted_street_address
+    assert_equal "4 Pennsylvania Plaza", madison_square_garden.street_address
+  end
+
+  def test_google_places_details_result_contains_place_id
+    assert_equal "ChIJhRwB-yFawokR5Phil-QQ3zM", madison_square_garden.place_id
+  end
+
+  def test_google_places_details_result_contains_latitude
+    assert_equal madison_square_garden.latitude, 40.750504
+  end
+
+  def test_google_places_details_result_contains_longitude
+    assert_equal madison_square_garden.longitude, -73.993439
+  end
+
+  def test_google_places_details_result_contains_rating
+    assert_equal 4.4, madison_square_garden.rating
+  end
+
+  def test_google_places_details_result_contains_rating_count
+    assert_equal 382, madison_square_garden.rating_count
+  end
+
+  def test_google_places_details_result_contains_reviews
+    reviews = madison_square_garden.reviews
+
+    assert_equal 2, reviews.size
+    assert_equal "John Smith", reviews.first["author_name"]
+    assert_equal 5, reviews.first["rating"]
+    assert_equal "It's nice.", reviews.first["text"]
+    assert_equal "en", reviews.first["language"]
+  end
+
+  def test_google_places_details_result_contains_types
+    assert_equal madison_square_garden.types, %w(stadium establishment)
+  end
+
+  def test_google_places_details_result_contains_website
+    assert_equal madison_square_garden.website, "http://www.thegarden.com/"
+  end
+
+  def test_google_places_details_result_contains_phone_number
+    assert_equal madison_square_garden.phone_number, "+1 212-465-6741"
+  end
+
+  def test_google_places_details_query_url_contains_placeid
+    url = lookup.query_url(Geocoder::Query.new("some-place-id"))
+    assert_match(/placeid=some-place-id/, url)
+  end
+
+  def test_google_places_details_query_url_contains_language
+    url = lookup.query_url(Geocoder::Query.new("some-place-id", language: "de"))
+    assert_match(/language=de/, url)
+  end
+
+  def test_google_places_details_query_url_always_uses_https
+    url = lookup.query_url(Geocoder::Query.new("some-place-id"))
+    assert_match(%r(^https://), url)
+  end
+
+  def test_google_places_details_query_url_omits_fields_by_default
+    url = lookup.query_url(Geocoder::Query.new("some-place-id"))
+    assert_no_match(/fields=/, url)
+  end
+
+  def test_google_places_details_query_url_contains_specific_fields_when_given
+    fields = %w[formatted_address place_id]
+    url = lookup.query_url(Geocoder::Query.new("some-place-id", fields: fields))
+    assert_match(/fields=#{fields.join('%2C')}/, url)
+  end
+
+  def test_google_places_details_query_url_contains_specific_fields_when_configured
+    fields = %w[business_status geometry photos]
+    Geocoder.configure(google_places_details: {fields: fields})
+    url = lookup.query_url(Geocoder::Query.new("some-place-id"))
+    assert_match(/fields=#{fields.join('%2C')}/, url)
+    Geocoder.configure(google_places_details: {})
+  end
+
+  def test_google_places_details_query_url_omits_fields_when_nil_given
+    Geocoder.configure(google_places_details: {fields: %w[business_status geometry photos]})
+    url = lookup.query_url(Geocoder::Query.new("some-place-id", fields: nil))
+    assert_no_match(/fields=/, url)
+    Geocoder.configure(google_places_details: {})
+  end
+
+  def test_google_places_details_query_url_omits_fields_when_nil_configured
+    Geocoder.configure(google_places_details: {fields: nil})
+    url = lookup.query_url(Geocoder::Query.new("some-place-id"))
+    assert_no_match(/fields=/, url)
+    Geocoder.configure(google_places_details: {})
+  end
+
+  def test_google_places_details_result_with_no_reviews_shows_empty_reviews
+    assert_equal no_reviews_result.reviews, []
+  end
+
+  def test_google_places_details_result_with_no_types_shows_empty_types
+    assert_equal no_types_result.types, []
+  end
+
+  def test_google_places_details_result_with_invalid_place_id_empty
+    silence_warnings do
+      assert_equal Geocoder.search("invalid request"), []
+    end
+  end
+
+  def test_raises_exception_on_google_places_details_invalid_request
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search("invalid request")
+    end
+  end
+
+  private
+
+  def lookup
+    Geocoder::Lookup::GooglePlacesDetails.new
+  end
+
+  def madison_square_garden
+    Geocoder.search("ChIJhRwB-yFawokR5Phil-QQ3zM").first
+  end
+
+  def no_reviews_result
+    Geocoder.search("no reviews").first
+  end
+
+  def no_types_result
+    Geocoder.search("no types").first
+  end
+
+end
diff --git a/test/unit/lookups/google_places_search_test.rb b/test/unit/lookups/google_places_search_test.rb
new file mode 100644
index 0000000..817a220
--- /dev/null
+++ b/test/unit/lookups/google_places_search_test.rb
@@ -0,0 +1,132 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GooglePlacesSearchTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :google_places_search)
+    set_api_key!(:google_places_search)
+  end
+
+  def test_google_places_search_result_contains_place_id
+    assert_equal "ChIJhRwB-yFawokR5Phil-QQ3zM", madison_square_garden.place_id
+  end
+
+  def test_google_places_search_result_contains_latitude
+    assert_equal madison_square_garden.latitude, 40.75050450000001
+  end
+
+  def test_google_places_search_result_contains_longitude
+    assert_equal madison_square_garden.longitude, -73.9934387
+  end
+
+  def test_google_places_search_result_contains_rating
+    assert_equal 4.5, madison_square_garden.rating
+  end
+
+  def test_google_places_search_result_contains_types
+    assert_equal madison_square_garden.types, %w(stadium point_of_interest establishment)
+  end
+
+  def test_google_places_search_query_url_contains_language
+    url = lookup.query_url(Geocoder::Query.new("some-address", language: "de"))
+    assert_match(/language=de/, url)
+  end
+
+  def test_google_places_search_query_url_contains_input
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_match(/input=some-address/, url)
+  end
+
+  def test_google_places_search_query_url_contains_input_typer
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_match(/inputtype=textquery/, url)
+  end
+
+  def test_google_places_search_query_url_always_uses_https
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_match(%r{^https://}, url)
+  end
+
+  def test_google_places_search_query_url_contains_every_field_available_by_default
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    fields = %w[id reference business_status formatted_address geometry icon name 
+      photos place_id plus_code types opening_hours price_level rating 
+      user_ratings_total]
+    assert_match(/fields=#{fields.join('%2C')}/, url)
+  end
+
+  def test_google_places_search_query_url_contains_specific_fields_when_given
+    fields = %w[formatted_address place_id]
+    url = lookup.query_url(Geocoder::Query.new("some-address", fields: fields))
+    
+    assert_match(/fields=#{fields.join('%2C')}/, url)
+  end
+
+  def test_google_places_search_query_url_contains_specific_fields_when_configured
+    fields = %w[business_status geometry photos]
+    Geocoder.configure(google_places_search: {fields: fields})
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_match(/fields=#{fields.join('%2C')}/, url)
+    Geocoder.configure(google_places_search: {})
+  end
+
+  def test_google_places_search_query_url_omits_fields_when_nil_given
+    url = lookup.query_url(Geocoder::Query.new("some-address", fields: nil))
+    assert_no_match(/fields=/, url)
+  end
+
+  def test_google_places_search_query_url_omits_fields_when_nil_configured
+    Geocoder.configure(google_places_search: {fields: nil})
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_no_match(/fields=/, url)
+    Geocoder.configure(google_places_search: {})
+  end
+
+  def test_google_places_search_query_url_omits_locationbias_by_default
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_no_match(/locationbias=/, url)
+  end
+
+  def test_google_places_search_query_url_contains_locationbias_when_configured
+    Geocoder.configure(google_places_search: {locationbias: "point:-36.8509,174.7645"})
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_match(/locationbias=point%3A-36.8509%2C174.7645/, url)
+    Geocoder.configure(google_places_search: {})
+  end
+
+  def test_google_places_search_query_url_contains_locationbias_when_given
+    url = lookup.query_url(Geocoder::Query.new("some-address", locationbias: "point:-36.8509,174.7645"))
+    assert_match(/locationbias=point%3A-36.8509%2C174.7645/, url)
+  end
+
+  def test_google_places_search_query_url_uses_given_locationbias_over_configured
+    Geocoder.configure(google_places_search: {locationbias: "point:37.4275,-122.1697"})
+    url = lookup.query_url(Geocoder::Query.new("some-address", locationbias: "point:-36.8509,174.7645"))
+    assert_match(/locationbias=point%3A-36.8509%2C174.7645/, url)
+    Geocoder.configure(google_places_search: {})
+  end
+
+  def test_google_places_search_query_url_omits_locationbias_when_nil_given
+    Geocoder.configure(google_places_search: {locationbias: "point:37.4275,-122.1697"})
+    url = lookup.query_url(Geocoder::Query.new("some-address", locationbias: nil))
+    assert_no_match(/locationbias=/, url)
+    Geocoder.configure(google_places_search: {})
+  end
+
+  def test_google_places_search_query_url_uses_find_place_service
+    url = lookup.query_url(Geocoder::Query.new("some-address"))
+    assert_match(%r{//maps.googleapis.com/maps/api/place/findplacefromtext/}, url)
+  end
+
+  private
+
+  def lookup
+    Geocoder::Lookup::GooglePlacesSearch.new
+  end
+
+  def madison_square_garden
+    Geocoder.search("Madison Square Garden").first
+  end
+end
diff --git a/test/unit/lookups/google_premier_test.rb b/test/unit/lookups/google_premier_test.rb
new file mode 100644
index 0000000..4b4e094
--- /dev/null
+++ b/test/unit/lookups/google_premier_test.rb
@@ -0,0 +1,30 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GooglePremierTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :google_premier)
+    set_api_key!(:google_premier)
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Manhattan", result.address_components_of_type(:sublocality).first['long_name']
+  end
+
+  def test_query_url
+    Geocoder.configure(google_premier: {api_key: ["deadbeef", "gme-test", "test-dev"]})
+    query = Geocoder::Query.new("Madison Square Garden, New York, NY")
+    assert_equal "https://maps.googleapis.com/maps/api/geocode/json?address=Madison+Square+Garden%2C+New+York%2C+NY&channel=test-dev&client=gme-test&language=en&sensor=false&signature=doJvJqX7YJzgV9rJ0DnVkTGZqTg=", query.url
+  end
+
+  def test_cache_key
+    Geocoder.configure(google_premier: {api_key: ["deadbeef", "gme-test", "test-dev"]})
+    lookup = Geocoder::Lookup.get(:google_premier)
+    query = Geocoder::Query.new("Madison Square Garden, New York, NY")
+    cache_key = lookup.send(:cache_key, query)
+    assert_equal "https://maps.googleapis.com/maps/api/geocode/json?address=Madison+Square+Garden%2C+New+York%2C+NY&language=en&sensor=false", cache_key
+  end
+end
diff --git a/test/unit/lookups/google_test.rb b/test/unit/lookups/google_test.rb
new file mode 100644
index 0000000..d464704
--- /dev/null
+++ b/test/unit/lookups/google_test.rb
@@ -0,0 +1,145 @@
+# encoding: utf-8
+require 'test_helper'
+
+class GoogleTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :google)
+  end
+
+  def test_google_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Manhattan",
+      result.address_components_of_type(:sublocality).first['long_name']
+  end
+
+  def test_google_result_components_contains_route
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Penn Plaza",
+      result.address_components_of_type(:route).first['long_name']
+  end
+
+  def test_google_result_components_contains_street_number
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "4",
+      result.address_components_of_type(:street_number).first['long_name']
+  end
+
+  def test_google_returns_city_when_no_locality_in_result
+    result = Geocoder.search("no locality").first
+    assert_equal "Haram", result.city
+  end
+
+  def test_google_city_results_returns_nil_if_no_matching_component_types
+    result = Geocoder.search("no city data").first
+    assert_equal nil, result.city
+  end
+
+  def test_google_street_address_returns_formatted_street_address
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "4 Penn Plaza", result.street_address
+  end
+
+  def test_google_precision
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "ROOFTOP",
+      result.precision
+  end
+
+  def test_google_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.7473324, -73.9965316, 40.7536276, -73.9902364],
+      result.viewport
+  end
+
+  def test_google_bounds
+    result = Geocoder.search("new york").first
+    assert_equal [40.4773991, -74.2590899, 40.9175771, -73.7002721],
+      result.bounds
+  end
+
+  def test_google_no_bounds
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal nil, result.bounds
+  end
+
+  def test_google_query_url_contains_bounds
+    lookup = Geocoder::Lookup::Google.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :bounds => [[40.0, -120.0], [39.0, -121.0]]
+    ))
+    assert_match(/bounds=40.0+%2C-120.0+%7C39.0+%2C-121.0+/, url)
+  end
+
+  def test_google_query_url_contains_region
+    lookup = Geocoder::Lookup::Google.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :region => "gb"
+    ))
+    assert_match(/region=gb/, url)
+  end
+
+  def test_google_query_url_contains_components_when_given_as_string
+    lookup = Geocoder::Lookup::Google.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :components => "locality:ES"
+    ))
+    formatted = "components=" + CGI.escape("locality:ES")
+    assert url.include?(formatted), "Expected #{formatted} to be included in #{url}"
+  end
+
+  def test_google_query_url_contains_components_when_given_as_array
+    lookup = Geocoder::Lookup::Google.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :components => ["country:ES", "locality:ES"]
+    ))
+    formatted = "components=" + CGI.escape("country:ES|locality:ES")
+    assert url.include?(formatted), "Expected #{formatted} to be included in #{url}"
+  end
+
+  def test_google_query_url_contains_result_type_when_given_as_string
+    lookup = Geocoder::Lookup::Google.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :result_type => "country"
+    ))
+    formatted = "result_type=" + CGI.escape("country")
+    assert url.include?(formatted), "Expected #{formatted} to be included in #{url}"
+  end
+
+  def test_google_query_url_contains_result_type_when_given_as_array
+    lookup = Geocoder::Lookup::Google.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :result_type => ["country", "postal_code"]
+    ))
+    formatted = "result_type=" + CGI.escape("country|postal_code")
+    assert url.include?(formatted), "Expected #{formatted} to be included in #{url}"
+  end
+
+  def test_google_uses_https_when_api_key_is_set
+    Geocoder.configure(api_key: "deadbeef")
+    query = Geocoder::Query.new("Madison Square Garden, New York, NY")
+    assert_match(/^https:/, query.url)
+  end
+
+  def test_actual_make_api_request_with_https
+    Geocoder.configure(use_https: true, lookup: :google)
+
+    require 'webmock/test_unit'
+    WebMock.enable!
+    stub_all = WebMock.stub_request(:any, /.*/).to_return(status: 200)
+
+    g = Geocoder::Lookup::Google.new
+    g.send(:actual_make_api_request, Geocoder::Query.new('test location'))
+    assert_requested(stub_all)
+
+    WebMock.reset!
+    WebMock.disable!
+  end
+end
diff --git a/test/unit/lookups/here_test.rb b/test/unit/lookups/here_test.rb
new file mode 100644
index 0000000..f83a0cf
--- /dev/null
+++ b/test/unit/lookups/here_test.rb
@@ -0,0 +1,74 @@
+# encoding: utf-8
+require 'test_helper'
+
+class HereTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :here)
+    set_api_key!(:here)
+  end
+
+  def test_with_array_api_key_raises_when_configured
+    Geocoder.configure(api_key: %w[foo bar])
+    Geocoder.configure(always_raise: :all)
+    assert_raises(Geocoder::ConfigurationError) { Geocoder.search('berlin').first }
+  end
+
+  def test_here_viewport
+    result = Geocoder.search('berlin').first
+    assert_equal [52.33812, 13.08835, 52.6755, 13.761], result.viewport
+  end
+
+  def test_here_no_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [], result.viewport
+  end
+
+  def test_here_query_url_for_reverse_geocoding
+    lookup = Geocoder::Lookup::Here.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        "42.42,21.21"
+      )
+    )
+
+    expected = /revgeocode\.search\.hereapi\.com\/v1\/revgeocode.+at=42\.42%2C21\.21/
+
+    assert_match(expected, url)
+  end
+
+  def test_here_query_url_for_geocode
+    lookup = Geocoder::Lookup::Here.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        "Madison Square Garden, New York, NY"
+      )
+    )
+
+    expected = /geocode\.search\.hereapi\.com\/v1\/geocode.+q=Madison\+Square\+Garden%2C\+New\+York%2C\+NY/
+
+    assert_match(expected, url)
+  end
+
+  def test_here_query_url_contains_country
+    lookup = Geocoder::Lookup::Here.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Some Intersection',
+        country: 'GBR'
+      )
+    )
+    assert_match(/in=countryCode%3AGBR/, url)
+  end
+
+  def test_here_query_url_contains_api_key
+    lookup = Geocoder::Lookup::Here.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Some Intersection'
+      )
+    )
+    assert_match(/apiKey=+/, url)
+  end
+end
diff --git a/test/unit/lookups/ip2location_test.rb b/test/unit/lookups/ip2location_test.rb
new file mode 100644
index 0000000..1e5b0bd
--- /dev/null
+++ b/test/unit/lookups/ip2location_test.rb
@@ -0,0 +1,45 @@
+# encoding: utf-8
+require 'test_helper'
+
+class Ip2locationTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :ip2location)
+    set_api_key!(:ip2location)
+  end
+
+  def test_ip2location_query_url
+    query = Geocoder::Query.new('8.8.8.8')
+    assert_equal 'http://api.ip2location.com/v2/?ip=8.8.8.8&key=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', query.url
+  end
+
+  def test_ip2location_query_url_with_package
+    Geocoder.configure(ip2location: {package: 'WS3'})
+    query = Geocoder::Query.new('8.8.8.8')
+    assert_equal 'http://api.ip2location.com/v2/?ip=8.8.8.8&key=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&package=WS3', query.url
+  end
+
+  def test_ip2location_lookup_address
+    result = Geocoder.search("8.8.8.8").first
+    assert_equal "US", result.country_code
+  end
+
+  def test_ip2location_lookup_loopback_address
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "INVALID IP ADDRESS", result.country_code
+  end
+
+  def test_ip2location_lookup_private_address
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "INVALID IP ADDRESS", result.country_code
+  end
+
+  def test_ip2location_extra_data
+    Geocoder.configure(:ip2location => {:package => "WS3"})
+    result = Geocoder.search("8.8.8.8").first
+    assert_equal "United States", result.country_name
+    assert_equal "California", result.region_name
+    assert_equal "Mountain View", result.city_name
+  end
+end
diff --git a/test/unit/lookups/ipapi_com_test.rb b/test/unit/lookups/ipapi_com_test.rb
new file mode 100644
index 0000000..5c33b06
--- /dev/null
+++ b/test/unit/lookups/ipapi_com_test.rb
@@ -0,0 +1,97 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpapiComTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :ipapi_com)
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::IpapiCom)
+  end
+
+  def test_result_components
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Jersey City, NJ 07302, United States", result.address
+  end
+
+  def test_all_api_fields
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal("United States", result.country)
+    assert_equal("US", result.country_code)
+    assert_equal("NJ", result.region)
+    assert_equal("New Jersey", result.region_name)
+    assert_equal("Jersey City", result.city)
+    assert_equal("07302", result.zip)
+    assert_equal(40.7209, result.lat)
+    assert_equal(-74.0468, result.lon)
+    assert_equal("America/New_York", result.timezone)
+    assert_equal("DataPipe", result.isp)
+    assert_equal("DataPipe", result.org)
+    assert_equal("AS22576 DataPipe, Inc.", result.as)
+    assert_equal("", result.reverse)
+    assert_equal(false, result.mobile)
+    assert_equal(false, result.proxy)
+    assert_equal("74.200.247.59", result.query)
+    assert_equal("success", result.status)
+    assert_equal(nil, result.message)
+  end
+
+  def test_loopback
+    result = Geocoder.search("::1").first
+    assert_equal(nil, result.lat)
+    assert_equal(nil, result.lon)
+    assert_equal([nil, nil], result.coordinates)
+    assert_equal(nil, result.reverse)
+    assert_equal("::1", result.query)
+    assert_equal("fail", result.status)
+  end
+
+  def test_private
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal nil, result.lat
+    assert_equal nil, result.lon
+    assert_equal [nil, nil], result.coordinates
+    assert_equal nil, result.reverse
+    assert_equal "172.19.0.1", result.query
+    assert_equal "fail", result.status
+  end
+
+  def test_api_key
+    Geocoder.configure(:api_key => "MY_KEY")
+    g = Geocoder::Lookup::IpapiCom.new
+    assert_match "key=MY_KEY", g.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+
+  def test_url_with_api_key_and_fields
+    Geocoder.configure(:api_key => "MY_KEY", :ipapi_com => {:fields => "lat,lon,xyz"})
+    g = Geocoder::Lookup::IpapiCom.new
+    assert_equal "http://pro.ip-api.com/json/74.200.247.59?fields=lat%2Clon%2Cxyz&key=MY_KEY", g.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+
+  def test_url_with_fields
+    Geocoder.configure(:ipapi_com => {:fields => "lat,lon"})
+    g = Geocoder::Lookup::IpapiCom.new
+    assert_equal "http://ip-api.com/json/74.200.247.59?fields=lat%2Clon", g.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+
+  def test_url_without_fields
+    g = Geocoder::Lookup::IpapiCom.new
+    assert_equal "http://ip-api.com/json/74.200.247.59", g.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+
+  def test_search_with_params
+    g = Geocoder::Lookup::IpapiCom.new
+    q = Geocoder::Query.new("74.200.247.59", :params => {:fields => 'lat,zip'})
+    assert_equal "http://ip-api.com/json/74.200.247.59?fields=lat%2Czip", g.query_url(q)
+  end
+
+  def test_use_https_with_api_key
+    Geocoder.configure(:api_key => "MY_KEY", :use_https => true)
+    g = Geocoder::Lookup::IpapiCom.new
+    assert_equal "https://pro.ip-api.com/json/74.200.247.59?key=MY_KEY", g.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+end
diff --git a/test/unit/lookups/ipbase_test.rb b/test/unit/lookups/ipbase_test.rb
new file mode 100644
index 0000000..f1619e6
--- /dev/null
+++ b/test/unit/lookups/ipbase_test.rb
@@ -0,0 +1,48 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpbaseTest < GeocoderTestCase
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :ipbase, lookup: :ipbase)
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+  def test_no_data
+    results = Geocoder.search("no data")
+    assert_equal 0, results.length
+  end
+
+  def test_invalid_ip
+    results = Geocoder.search("invalid ip")
+    assert_equal 0, results.length
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::Ipbase)
+  end
+
+  def test_result_on_loopback_ip_address_search
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal 'RD',        result.country_code
+    assert_equal "Reserved",  result.country
+  end
+
+  def test_result_on_private_ip_address_search
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "172.19.0.1", result.ip
+    assert_equal 'RD',         result.country_code
+    assert_equal "Reserved",   result.country
+  end
+
+  def test_result_components
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Jersey City, New Jersey 07302, United States", result.address
+  end
+end
diff --git a/test/unit/lookups/ipdata_co_test.rb b/test/unit/lookups/ipdata_co_test.rb
new file mode 100644
index 0000000..2a8c928
--- /dev/null
+++ b/test/unit/lookups/ipdata_co_test.rb
@@ -0,0 +1,69 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpdataCoTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :ipdata_co)
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::IpdataCo)
+  end
+
+  def test_result_on_loopback_ip_address_search
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal 'RD',        result.country_code
+    assert_equal "Reserved",  result.country
+  end
+
+  def test_result_on_private_ip_address_search
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "172.19.0.1", result.ip
+    assert_equal 'RD',         result.country_code
+    assert_equal "Reserved",   result.country
+  end
+
+  def test_invalid_json
+    Geocoder.configure(:always_raise => [Geocoder::ResponseParseError])
+    assert_raise Geocoder::ResponseParseError do
+      Geocoder.search("8.8.8", ip_address: true)
+    end
+  end
+
+  def test_result_components
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Jersey City, NJ 07302, United States", result.address
+  end
+
+  def test_not_authorized
+    Geocoder.configure(always_raise: [Geocoder::RequestDenied])
+    lookup = Geocoder::Lookup.get(:ipdata_co)
+      assert_raises Geocoder::RequestDenied do
+        response = MockHttpResponse.new(code: 403)
+        lookup.send(:check_response_for_errors!, response)
+    end
+  end
+
+  def test_api_key
+    Geocoder.configure(:api_key => 'XXXX')
+
+    # HACK: run the code once to add the api key to the HTTP request headers
+    Geocoder.search('8.8.8.8')
+    # It's really hard to 'un-monkey-patch' the base lookup class here
+
+    require 'webmock/test_unit'
+    WebMock.enable!
+    stubbed_request = WebMock.stub_request(:get, "https://api.ipdata.co/8.8.8.8?api-key=XXXX").to_return(status: 200)
+
+    g = Geocoder::Lookup::IpdataCo.new
+    g.send(:actual_make_api_request, Geocoder::Query.new('8.8.8.8'))
+    assert_requested(stubbed_request)
+
+    WebMock.reset!
+    WebMock.disable!
+  end
+end
diff --git a/test/unit/lookups/ipgeolocation_test.rb b/test/unit/lookups/ipgeolocation_test.rb
new file mode 100644
index 0000000..3307afc
--- /dev/null
+++ b/test/unit/lookups/ipgeolocation_test.rb
@@ -0,0 +1,116 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpgeolocationTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(
+        :api_key => 'ea91e4a4159247fdb0926feae70c2911',
+        :ip_lookup => :ipgeolocation,
+        :always_raise => :all
+    )
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("103.217.177.217").first
+    assert result.is_a?(Geocoder::Result::Ipgeolocation)
+  end
+
+  def test_result_components
+    result = Geocoder.search("103.217.177.217").first
+    assert_equal "Pakistan", result.country_name
+  end
+
+  def test_all_top_level_api_fields
+    result = Geocoder.search("103.217.177.217").first
+    assert_equal "103.217.177.217", result.ip
+    assert_equal "AS",              result.continent_code
+    assert_equal "Asia",            result.continent_name
+    assert_equal "PK",              result.country_code2
+    assert_equal "Pakistan",        result.country_name
+    assert_equal "Islamabad",       result.city
+    assert_equal "44000",           result.zipcode
+    assert_equal 33.7334,           result.latitude
+    assert_equal 73.0785,           result.longitude
+  end
+
+  def test_nested_api_fields
+    result = Geocoder.search("103.217.177.217").first
+
+    assert result.time_zone.is_a?(Hash)
+    assert_equal "Asia/Karachi", result.time_zone['name']
+
+    assert result.currency.is_a?(Hash)
+    assert_equal "PKR", result.currency['code']
+  end
+
+  def test_required_base_fields
+    result = Geocoder.search("103.217.177.217").first
+
+    assert_equal "Islamabad",        result.country_capital
+    assert_equal "Islamabad",        result.state_prov
+    assert_equal "Islamabad",        result.city
+    assert_equal "44000",            result.zipcode
+    assert_equal [33.7334, 73.0785], result.coordinates
+  end
+
+  def test_localhost_loopback
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal "RD",        result.country_code2
+    assert_equal "Reserved",  result.country_name
+  end
+
+  def test_localhost_loopback_defaults
+    result = Geocoder.search("127.0.0.1").first
+
+    assert_equal "127.0.0.1", result.ip
+    assert_equal "",          result.continent_code
+    assert_equal "",          result.continent_name
+    assert_equal "RD",          result.country_code2
+    assert_equal "Reserved",  result.country_name
+    assert_equal "",          result.city
+    assert_equal "",          result.zipcode
+    assert_equal 0,           result.latitude
+    assert_equal 0,           result.longitude
+    assert_equal({},          result.time_zone)
+    assert_equal({},          result.currency)
+  end
+
+  def test_localhost_private
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "172.19.0.1", result.ip
+    assert_equal "RD",           result.country_code2
+    assert_equal "Reserved",   result.country_name
+  end
+
+  def test_api_request_adds_access_key
+    lookup = Geocoder::Lookup.get(:ipgeolocation)
+    assert_match(/apiKey=\w+/, lookup.query_url(Geocoder::Query.new("74.200.247.59")))
+  end
+
+  def test_api_request_adds_security_when_specified
+    lookup = Geocoder::Lookup.get(:ipgeolocation)
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { security: '1' }))
+    assert_match(/&security=1/, query_url)
+  end
+
+  def test_api_request_adds_hostname_when_specified
+    lookup = Geocoder::Lookup.get(:ipgeolocation)
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { hostname: '1' }))
+    assert_match(/&hostname=1/, query_url)
+  end
+
+  def test_api_request_adds_language_when_specified
+    lookup = Geocoder::Lookup.get(:ipgeolocation)
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { language: 'es' }))
+    assert_match(/&language=es/, query_url)
+  end
+
+  def test_api_request_adds_fields_when_specified
+    lookup = Geocoder::Lookup.get(:ipgeolocation)
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { fields: 'foo,bar' }))
+    assert_match(/&fields=foo%2Cbar/, query_url)
+  end
+end
diff --git a/test/unit/lookups/ipinfo_io_test.rb b/test/unit/lookups/ipinfo_io_test.rb
new file mode 100644
index 0000000..b75b461
--- /dev/null
+++ b/test/unit/lookups/ipinfo_io_test.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpinfoIoTest < GeocoderTestCase
+
+  def test_ipinfo_io_lookup_loopback_address
+    Geocoder.configure(:ip_lookup => :ipinfo_io)
+    result = Geocoder.search("127.0.0.1").first
+    assert_nil result.latitude
+    assert_nil result.longitude
+    assert_equal "127.0.0.1", result.ip
+  end
+
+  def test_ipinfo_io_lookup_private_address
+    Geocoder.configure(:ip_lookup => :ipinfo_io)
+    result = Geocoder.search("172.19.0.1").first
+    assert_nil result.latitude
+    assert_nil result.longitude
+    assert_equal "172.19.0.1", result.ip
+  end
+
+  def test_ipinfo_io_extra_attributes
+    Geocoder.configure(:ip_lookup => :ipinfo_io, :use_https => true)
+    result = Geocoder.search("8.8.8.8").first
+    assert_equal "8.8.8.8", result.ip
+    assert_equal "California", result.region
+    assert_equal "94040", result.postal
+  end
+end
diff --git a/test/unit/lookups/ipqualityscore_test.rb b/test/unit/lookups/ipqualityscore_test.rb
new file mode 100644
index 0000000..0bdbdc2
--- /dev/null
+++ b/test/unit/lookups/ipqualityscore_test.rb
@@ -0,0 +1,112 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpqualityscoreTest < GeocoderTestCase
+
+  def setup
+    super
+    # configuring this IP lookup as the address lookup is weird, but necessary
+    # in order to run tests with the 'quota exceeded' fixture
+    Geocoder.configure(lookup: :ipqualityscore, ip_lookup: :ipqualityscore)
+    set_api_key!(:ipqualityscore)
+  end
+
+  def test_result_attributes
+    result = Geocoder.search('74.200.247.59').first
+
+    # Request
+    assert_equal('3YqddtowOADDvCm', result.request_id)
+    assert_equal(true, result.success?)
+    assert_equal('Success', result.message)
+
+    # Geolocation
+    assert_equal(40.73, result.latitude)
+    assert_equal(-74.04, result.longitude)
+    assert_equal 'Jersey City, New Jersey, US', result.address
+    assert_equal('Jersey City', result.city)
+    assert_equal('New Jersey', result.state)
+    assert_equal('US', result.country_code)
+
+    # Fallbacks for data API doesn't provide
+    assert_equal('New Jersey', result.state_code)
+    assert_equal('New Jersey', result.province_code)
+    assert_equal('', result.postal_code)
+    assert_equal('US', result.country)
+
+    # Security
+    assert_equal(false, result.mobile?)
+    assert_equal(78, result.fraud_score)
+    assert_equal('Rackspace Hosting', result.isp)
+    assert_equal(19994, result.asn)
+    assert_equal('Rackspace Hosting', result.organization)
+    assert_equal(false, result.crawler?)
+    assert_equal('74.200.247.59', result.host)
+    assert_equal(true, result.proxy?)
+    assert_equal(true, result.vpn?)
+    assert_equal(false, result.tor?)
+    assert_equal(false, result.active_vpn?)
+    assert_equal(false, result.active_tor?)
+    assert_equal(false, result.recent_abuse?)
+    assert_equal(false, result.bot?)
+    assert_equal('Corporate', result.connection_type)
+    assert_equal('low', result.abuse_velocity)
+
+    # Timezone
+    assert_equal('America/New_York', result.timezone)
+  end
+
+  def test_raises_invalid_api_key_exception
+    Geocoder.configure always_raise: :all
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder::Lookup::Ipqualityscore.new.send(:results, Geocoder::Query.new('invalid api key'))
+    end
+  end
+
+  def test_raises_invalid_request_exception
+    Geocoder.configure always_raise: :all
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder::Lookup::Ipqualityscore.new.send(:results, Geocoder::Query.new('invalid request'))
+    end
+  end
+
+  def test_raises_over_query_limit_exception_insufficient_credits
+    Geocoder.configure always_raise: :all
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder::Lookup::Ipqualityscore.new.send(:results, Geocoder::Query.new('insufficient credits'))
+    end
+  end
+
+  def test_raises_over_query_limit_exception_quota_exceeded
+    Geocoder.configure always_raise: :all
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder::Lookup::Ipqualityscore.new.send(:results, Geocoder::Query.new('quota exceeded'))
+    end
+  end
+
+  def test_unsuccessful_response_without_raising_does_not_hit_cache
+    Geocoder.configure(cache: {}, always_raise: [])
+    lookup = Geocoder::Lookup.get(:ipqualityscore)
+
+    Geocoder.search('quota exceeded')
+    assert_false lookup.instance_variable_get(:@cache_hit)
+
+    Geocoder.search('quota exceeded')
+    assert_false lookup.instance_variable_get(:@cache_hit)
+  end
+
+  def test_unsuccessful_response_with_raising_does_not_hit_cache
+    Geocoder.configure(cache: {}, always_raise: [Geocoder::OverQueryLimitError])
+    lookup = Geocoder::Lookup.get(:ipqualityscore)
+
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search('quota exceeded')
+    end
+    assert_false lookup.instance_variable_get(:@cache_hit)
+
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search('quota exceeded')
+    end
+    assert_false lookup.instance_variable_get(:@cache_hit)
+  end
+
+end
diff --git a/test/unit/lookups/ipregistry_test.rb b/test/unit/lookups/ipregistry_test.rb
new file mode 100644
index 0000000..e70dc34
--- /dev/null
+++ b/test/unit/lookups/ipregistry_test.rb
@@ -0,0 +1,32 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpregistryTest < GeocoderTestCase
+  def test_lookup_loopback_address
+    Geocoder.configure(:ip_lookup => :ipregistry)
+    result = Geocoder.search("127.0.0.1").first
+    assert_nil result.latitude
+    assert_nil result.longitude
+    assert_equal "127.0.0.1", result.ip
+  end
+
+  def test_lookup_private_address
+    Geocoder.configure(:ip_lookup => :ipregistry)
+    result = Geocoder.search("172.19.0.1").first
+    assert_nil result.latitude
+    assert_nil result.longitude
+    assert_equal "172.19.0.1", result.ip
+  end
+
+  def test_known_ip_address
+    Geocoder.configure(:ip_lookup => :ipregistry)
+    result = Geocoder.search("8.8.8.8").first
+    assert_equal "8.8.8.8", result.ip
+    assert_equal "California", result.state
+    assert_equal "USD", result.currency_code
+    assert_equal "NA", result.location_continent_code
+    assert_equal "US", result.location_country_code
+    assert_equal false, result.security_is_tor
+    assert_equal "America/Los_Angeles", result.time_zone_id
+  end
+end
diff --git a/test/unit/lookups/ipstack_test.rb b/test/unit/lookups/ipstack_test.rb
new file mode 100644
index 0000000..ef5c52a
--- /dev/null
+++ b/test/unit/lookups/ipstack_test.rb
@@ -0,0 +1,291 @@
+# encoding: utf-8
+require 'test_helper'
+
+class SpyLogger
+  def initialize
+    @log = []
+  end
+
+  def logged?(msg)
+    @log.include?(msg)
+  end
+
+  def add(level, msg)
+    @log << msg
+  end
+end
+
+class IpstackTest < GeocoderTestCase
+
+  def setup
+    super
+    @logger = SpyLogger.new
+    Geocoder.configure(
+      :api_key => '123',
+      :ip_lookup => :ipstack,
+      :always_raise => :all,
+      :logger => @logger
+    )
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("134.201.250.155").first
+    assert result.is_a?(Geocoder::Result::Ipstack)
+  end
+
+  def test_result_components
+    result = Geocoder.search("134.201.250.155").first
+    assert_equal "Los Angeles, CA 90013, United States", result.address
+  end
+
+  def test_all_top_level_api_fields
+    result = Geocoder.search("134.201.250.155").first
+    assert_equal "134.201.250.155", result.ip
+    assert_equal "134.201.250.155", result.hostname
+    assert_equal "NA",              result.continent_code
+    assert_equal "North America",   result.continent_name
+    assert_equal "US",              result.country_code
+    assert_equal "United States",   result.country_name
+    assert_equal "CA",              result.region_code
+    assert_equal "California",      result.region_name
+    assert_equal "Los Angeles",     result.city
+    assert_equal "90013",           result.zip
+    assert_equal 34.0453,           result.latitude
+    assert_equal (-118.2413),       result.longitude
+  end
+
+  def test_nested_api_fields
+    result = Geocoder.search("134.201.250.155").first
+
+    assert result.location.is_a?(Hash)
+    assert_equal 5368361, result.location['geoname_id']
+
+    assert result.time_zone.is_a?(Hash)
+    assert_equal "America/Los_Angeles", result.time_zone['id']
+
+    assert result.currency.is_a?(Hash)
+    assert_equal "USD", result.currency['code']
+
+    assert result.connection.is_a?(Hash)
+    assert_equal 25876, result.connection['asn']
+
+    assert result.security.is_a?(Hash)
+  end
+
+  def test_required_base_fields
+    result = Geocoder.search("134.201.250.155").first
+    assert_equal "California",      result.state
+    assert_equal "CA",              result.state_code
+    assert_equal "United States",   result.country
+    assert_equal "90013",           result.postal_code
+    assert_equal [34.0453, -118.2413], result.coordinates
+  end
+
+  def test_logs_deprecation_of_metro_code_field
+    result = Geocoder.search("134.201.250.155").first
+    result.metro_code
+
+    assert @logger.logged?("Ipstack does not implement `metro_code` in api results.  Please discontinue use.")
+  end
+
+  def test_localhost_loopback
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal "RD",        result.country_code
+    assert_equal "Reserved",  result.country_name
+  end
+
+  def test_localhost_loopback_defaults
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal "",          result.hostname
+    assert_equal "",          result.continent_code
+    assert_equal "",          result.continent_name
+    assert_equal "RD",        result.country_code
+    assert_equal "Reserved",  result.country_name
+    assert_equal "",          result.region_code
+    assert_equal "",          result.region_name
+    assert_equal "",          result.city
+    assert_equal "",          result.zip
+    assert_equal 0,           result.latitude
+    assert_equal 0,           result.longitude
+    assert_equal({},          result.location)
+    assert_equal({},          result.time_zone)
+    assert_equal({},          result.currency)
+    assert_equal({},          result.connection)
+  end
+
+  def test_localhost_private
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "172.19.0.1", result.ip
+    assert_equal "RD",         result.country_code
+    assert_equal "Reserved",   result.country_name
+  end
+
+  def test_api_request_adds_access_key
+    lookup = Geocoder::Lookup.get(:ipstack)
+    assert_match 'http://api.ipstack.com/74.200.247.59?access_key=123', lookup.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+
+  def test_api_request_adds_security_when_specified
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { security: '1' }))
+
+    assert_match(/&security=1/, query_url)
+  end
+
+  def test_api_request_adds_hostname_when_specified
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { hostname: '1' }))
+
+    assert_match(/&hostname=1/, query_url)
+  end
+
+  def test_api_request_adds_language_when_specified
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { language: 'es' }))
+
+    assert_match(/&language=es/, query_url)
+  end
+
+  def test_api_request_adds_fields_when_specified
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    query_url = lookup.query_url(Geocoder::Query.new("74.200.247.59", params: { fields: 'foo,bar' }))
+
+    assert_match(/&fields=foo%2Cbar/, query_url)
+  end
+
+  def test_logs_warning_when_errors_are_set_not_to_raise
+    Geocoder::Configuration.instance.data.clear
+    Geocoder::Configuration.set_defaults
+    Geocoder.configure(api_key: '123', ip_lookup: :ipstack, logger: @logger)
+
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    lookup.send(:results, Geocoder::Query.new("not_found"))
+
+    assert @logger.logged?("Ipstack Geocoding API error: The requested resource does not exist.")
+  end
+
+  def test_uses_lookup_specific_configuration
+    Geocoder::Configuration.instance.data.clear
+    Geocoder::Configuration.set_defaults
+    Geocoder.configure(api_key: '123', ip_lookup: :ipstack, logger: @logger, ipstack: { api_key: '345'})
+
+    lookup = Geocoder::Lookup.get(:ipstack)
+    assert_match 'http://api.ipstack.com/74.200.247.59?access_key=345', lookup.query_url(Geocoder::Query.new("74.200.247.59"))
+  end
+
+  def test_not_authorized   lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::InvalidRequest do
+      lookup.send(:results, Geocoder::Query.new("not_found"))
+    end
+
+    assert_equal error.message, "The requested resource does not exist."
+  end
+
+  def test_missing_access_key
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::InvalidApiKey do
+      lookup.send(:results, Geocoder::Query.new("missing_access_key"))
+    end
+
+    assert_equal error.message, "No API Key was specified."
+  end
+
+  def test_invalid_access_key
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::InvalidApiKey do
+      lookup.send(:results, Geocoder::Query.new("invalid_access_key"))
+    end
+
+    assert_equal error.message, "No API Key was specified or an invalid API Key was specified."
+  end
+
+  def test_inactive_user
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::Error do
+      lookup.send(:results, Geocoder::Query.new("inactive_user"))
+    end
+
+    assert_equal error.message, "The current user account is not active. User will be prompted to get in touch with Customer Support."
+  end
+
+  def test_invalid_api_function
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::InvalidRequest do
+      lookup.send(:results, Geocoder::Query.new("invalid_api_function"))
+    end
+
+    assert_equal error.message, "The requested API endpoint does not exist."
+  end
+
+  def test_usage_limit
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::OverQueryLimitError do
+      lookup.send(:results, Geocoder::Query.new("usage_limit"))
+    end
+
+    assert_equal error.message, "The maximum allowed amount of monthly API requests has been reached."
+  end
+
+  def test_access_restricted
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::RequestDenied do
+      lookup.send(:results, Geocoder::Query.new("access_restricted"))
+    end
+
+    assert_equal error.message, "The current subscription plan does not support this API endpoint."
+  end
+
+  def test_protocol_access_restricted
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::RequestDenied do
+      lookup.send(:results, Geocoder::Query.new("protocol_access_restricted"))
+    end
+
+    assert_equal error.message, "The user's current subscription plan does not support HTTPS Encryption."
+  end
+
+  def test_invalid_fields
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::InvalidRequest do
+      lookup.send(:results, Geocoder::Query.new("invalid_fields"))
+    end
+
+    assert_equal error.message, "One or more invalid fields were specified using the fields parameter."
+  end
+
+  def test_too_many_ips
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::InvalidRequest do
+      lookup.send(:results, Geocoder::Query.new("too_many_ips"))
+    end
+
+    assert_equal error.message, "Too many IPs have been specified for the Bulk Lookup Endpoint. (max. 50)"
+  end
+
+  def test_batch_not_supported
+    lookup = Geocoder::Lookup.get(:ipstack)
+
+    error = assert_raise Geocoder::RequestDenied do
+      lookup.send(:results, Geocoder::Query.new("batch_not_supported"))
+    end
+
+    assert_equal error.message, "The Bulk Lookup Endpoint is not supported on the current subscription plan"
+  end
+end
diff --git a/test/unit/lookups/latlon_test.rb b/test/unit/lookups/latlon_test.rb
new file mode 100644
index 0000000..41954c1
--- /dev/null
+++ b/test/unit/lookups/latlon_test.rb
@@ -0,0 +1,43 @@
+# encoding: utf-8
+require 'test_helper'
+
+class LatlonTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :latlon)
+    set_api_key!(:latlon)
+  end
+
+  def test_result_components
+    result = Geocoder.search("6000 Universal Blvd, Orlando, FL 32819").first
+    assert_equal "6000", result.number
+    assert_equal "Universal", result.street_name
+    assert_equal "Blvd", result.street_type
+    assert_equal "Universal Blvd", result.street
+    assert_equal "Orlando", result.city
+    assert_equal "FL", result.state
+    assert_equal "32819", result.zip
+    assert_equal "6000 Universal Blvd, Orlando, FL 32819", result.formatted_address
+    assert_equal(28.4750507575094, result.latitude)
+    assert_equal(-81.4630386931719, result.longitude)
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+  def test_latlon_reverse_url
+    query = Geocoder::Query.new([45.423733, -75.676333])
+    assert_match(/reverse_geocode/, query.url)
+  end
+
+  def test_raises_api_key_exception
+    Geocoder.configure Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid key")
+    end
+  end
+
+end
diff --git a/test/unit/lookups/location_iq_test.rb b/test/unit/lookups/location_iq_test.rb
new file mode 100644
index 0000000..8b63984
--- /dev/null
+++ b/test/unit/lookups/location_iq_test.rb
@@ -0,0 +1,46 @@
+# encoding: utf-8
+require 'unit/lookups/nominatim_test'
+require 'test_helper'
+
+class LocationIq < NominatimTest
+
+  def setup
+    super
+    Geocoder.configure(lookup: :location_iq)
+    set_api_key!(:location_iq)
+  end
+
+  def test_url_contains_api_key
+    Geocoder.configure(location_iq: {api_key: "abc123"})
+    query = Geocoder::Query.new("Leadville, CO")
+    assert_match(/key=abc123/, query.url)
+  end
+
+  def test_raises_exception_with_invalid_api_key
+    Geocoder.configure(always_raise: [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid api key")
+    end
+  end
+
+  def test_raises_exception_with_request_denied
+    Geocoder.configure(always_raise: [Geocoder::RequestDenied])
+    assert_raises Geocoder::RequestDenied do
+      Geocoder.search("request denied")
+    end
+  end
+
+  def test_raises_exception_with_rate_limited
+    Geocoder.configure(always_raise: [Geocoder::OverQueryLimitError])
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search("over limit")
+    end
+  end
+
+  def test_raises_exception_with_invalid_request
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search("invalid request")
+    end
+  end
+end
diff --git a/test/unit/lookups/mapbox_test.rb b/test/unit/lookups/mapbox_test.rb
new file mode 100644
index 0000000..efe9788
--- /dev/null
+++ b/test/unit/lookups/mapbox_test.rb
@@ -0,0 +1,61 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MapboxTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :mapbox)
+    set_api_key!(:mapbox)
+  end
+
+  def test_url_contains_api_key
+    Geocoder.configure(mapbox: {api_key: "abc123"})
+    query = Geocoder::Query.new("Leadville, CO")
+    assert_equal "https://api.mapbox.com/geocoding/v5/mapbox.places/Leadville%2C+CO.json?access_token=abc123", query.url
+  end
+
+  def test_url_contains_params
+    Geocoder.configure(mapbox: {api_key: "abc123"})
+    query = Geocoder::Query.new("Leadville, CO", {params: {country: 'CN'}})
+    assert_equal "https://api.mapbox.com/geocoding/v5/mapbox.places/Leadville%2C+CO.json?access_token=abc123&country=CN", query.url
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.750755, -73.993710125], result.coordinates
+    assert_equal "Madison Square Garden", result.place_name
+    assert_equal "4 Penn Plz", result.street
+    assert_equal "New York", result.city
+    assert_equal "New York", result.state
+    assert_equal "10119", result.postal_code
+    assert_equal "NY", result.state_code
+    assert_equal "United States", result.country
+    assert_equal "US", result.country_code
+    assert_equal "Garment District", result.neighborhood
+    assert_equal "Madison Square Garden, 4 Penn Plz, New York, New York, 10119, United States", result.address
+  end
+
+  def test_no_results
+    assert_equal [], Geocoder.search("no results")
+  end
+
+  def test_raises_exception_with_invalid_api_key
+    Geocoder.configure(always_raise: [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid api key")
+    end
+  end
+
+  def test_truncates_query_at_semicolon
+    result = Geocoder.search("Madison Square Garden, New York, NY;123 Another St").first
+    assert_equal [40.750755, -73.993710125], result.coordinates
+  end
+
+  def test_mapbox_result_without_context
+    assert_nothing_raised do
+      result = Geocoder.search("Shanghai, China")[0]
+      assert_equal nil, result.city
+    end
+  end
+end
diff --git a/test/unit/lookups/mapquest_test.rb b/test/unit/lookups/mapquest_test.rb
new file mode 100644
index 0000000..65114b3
--- /dev/null
+++ b/test/unit/lookups/mapquest_test.rb
@@ -0,0 +1,54 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MapquestTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :mapquest)
+    set_api_key!(:mapquest)
+  end
+
+  def test_url_contains_api_key
+    Geocoder.configure(mapquest: {api_key: "abc123"})
+    query = Geocoder::Query.new("Bluffton, SC")
+    assert_equal "http://www.mapquestapi.com/geocoding/v1/address?key=abc123&location=Bluffton%2C+SC", query.url
+  end
+
+  def test_url_for_open_street_maps
+    Geocoder.configure(mapquest: {api_key: "abc123", open: true})
+    query = Geocoder::Query.new("Bluffton, SC")
+    assert_equal "http://open.mapquestapi.com/geocoding/v1/address?key=abc123&location=Bluffton%2C+SC", query.url
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "10001", result.postal_code
+    assert_equal "46 West 31st Street, New York, NY, 10001, US", result.address
+  end
+
+  def test_no_results
+    assert_equal [], Geocoder.search("no results")
+  end
+
+  def test_raises_exception_when_invalid_request
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search("invalid request")
+    end
+  end
+
+  def test_raises_exception_when_invalid_api_key
+    Geocoder.configure(always_raise: [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid api key")
+    end
+  end
+
+  def test_raises_exception_when_error
+    Geocoder.configure(always_raise: [Geocoder::Error])
+    assert_raises Geocoder::Error do
+      Geocoder.search("error")
+    end
+  end
+end
diff --git a/test/unit/lookups/maxmind_geoip2_test.rb b/test/unit/lookups/maxmind_geoip2_test.rb
new file mode 100644
index 0000000..8ff2671
--- /dev/null
+++ b/test/unit/lookups/maxmind_geoip2_test.rb
@@ -0,0 +1,54 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MaxmindGeoip2Test < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :maxmind_geoip2)
+  end
+
+  def test_result_attributes
+    result = Geocoder.search('1.2.3.4').first
+    assert_equal 'Los Angeles, CA 90001, United States', result.address
+    assert_equal 'Los Angeles', result.city
+    assert_equal 'CA', result.state_code
+    assert_equal 'California', result.state
+    assert_equal 'United States', result.country
+    assert_equal 'US', result.country_code
+    assert_equal '90001', result.postal_code
+    assert_equal(37.6293, result.latitude)
+    assert_equal(-122.1163, result.longitude)
+    assert_equal [37.6293, -122.1163], result.coordinates
+  end
+
+  def test_loopback
+    results = Geocoder.search('127.0.0.1')
+    assert_equal [], results
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+  def test_dynamic_localization
+    result = Geocoder.search('1.2.3.4').first
+
+    result.language = :ru
+
+    assert_equal 'Лос-Анджелес', result.city
+    assert_equal 'Калифорния', result.state
+    assert_equal 'США', result.country
+  end
+
+  def test_dynamic_localization_fallback
+    result = Geocoder.search('1.2.3.4').first
+
+    result.language = :unsupported_language
+
+    assert_equal 'Los Angeles', result.city
+    assert_equal 'California', result.state
+    assert_equal 'United States', result.country
+  end
+end
diff --git a/test/unit/lookups/maxmind_local_test.rb b/test/unit/lookups/maxmind_local_test.rb
new file mode 100644
index 0000000..915b9fc
--- /dev/null
+++ b/test/unit/lookups/maxmind_local_test.rb
@@ -0,0 +1,28 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MaxmindLocalTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :maxmind_local)
+  end
+
+  def test_result_attributes
+    result = Geocoder.search('8.8.8.8').first
+    assert_equal 'Mountain View, CA 94043, United States', result.address
+    assert_equal 'Mountain View', result.city
+    assert_equal 'CA', result.state
+    assert_equal 'United States', result.country
+    assert_equal 'US', result.country_code
+    assert_equal '94043', result.postal_code
+    assert_equal(37.41919999999999, result.latitude)
+    assert_equal(-122.0574, result.longitude)
+    assert_equal [37.41919999999999, -122.0574], result.coordinates
+  end
+
+  def test_loopback
+    results = Geocoder.search('127.0.0.1')
+    assert_equal [], results
+  end
+end
diff --git a/test/unit/lookups/maxmind_test.rb b/test/unit/lookups/maxmind_test.rb
new file mode 100644
index 0000000..d02b7eb
--- /dev/null
+++ b/test/unit/lookups/maxmind_test.rb
@@ -0,0 +1,75 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MaxmindTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :maxmind)
+  end
+
+  def test_maxmind_result_on_ip_address_search
+    Geocoder.configure(maxmind: {service: :city_isp_org})
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::Maxmind)
+  end
+
+  def test_maxmind_result_knows_country_service_name
+    Geocoder.configure(maxmind: {service: :country})
+    assert_equal :country, Geocoder.search("24.24.24.21").first.service_name
+  end
+
+  def test_maxmind_result_knows_city_service_name
+    Geocoder.configure(maxmind: {service: :city})
+    assert_equal :city, Geocoder.search("24.24.24.22").first.service_name
+  end
+
+  def test_maxmind_result_knows_city_isp_org_service_name
+    Geocoder.configure(maxmind: {service: :city_isp_org})
+    assert_equal :city_isp_org, Geocoder.search("24.24.24.23").first.service_name
+  end
+
+  def test_maxmind_result_knows_omni_service_name
+    Geocoder.configure(maxmind: {service: :omni})
+    assert_equal :omni, Geocoder.search("24.24.24.24").first.service_name
+  end
+
+  def test_maxmind_special_result_components
+    Geocoder.configure(maxmind: {service: :omni})
+    result = Geocoder.search("24.24.24.24").first
+    assert_equal "Road Runner", result.isp_name
+    assert_equal "Cable/DSL", result.netspeed
+    assert_equal "rr.com", result.domain
+  end
+
+  def test_maxmind_raises_exception_when_service_not_configured
+    Geocoder.configure(maxmind: {service: nil})
+    assert_raises Geocoder::ConfigurationError do
+      Geocoder::Query.new("24.24.24.24").url
+    end
+  end
+
+  def test_maxmind_works_when_loopback_address_on_omni
+    Geocoder.configure(maxmind: {service: :omni})
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "", result.country_code
+  end
+
+  def test_maxmind_works_when_loopback_address_on_country
+    Geocoder.configure(maxmind: {service: :country})
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "", result.country_code
+  end
+
+  def test_maxmind_works_when_private_address_on_omni
+    Geocoder.configure(maxmind: {service: :omni})
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "", result.country_code
+  end
+
+  def test_maxmind_works_when_private_address_on_country
+    Geocoder.configure(maxmind: {service: :country})
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "", result.country_code
+  end
+end
diff --git a/test/unit/lookups/melissa_street_test.rb b/test/unit/lookups/melissa_street_test.rb
new file mode 100644
index 0000000..dc3ba5f
--- /dev/null
+++ b/test/unit/lookups/melissa_street_test.rb
@@ -0,0 +1,36 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MelissaStreetTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :melissa_street)
+    set_api_key!(:melissa_street)
+  end
+
+  def test_result_components
+    result = Geocoder.search("1 Frank H Ogawa Plz Fl 3").first
+    assert_equal "1", result.number
+    assert_equal "1 Frank H Ogawa Plz Fl 3", result.street_address
+    assert_equal "Plz", result.suffix
+    assert_equal "CA", result.state
+    assert_equal "94612-1932", result.postal_code
+    assert_equal "Oakland", result.city
+    assert_equal "US", result.country_code
+    assert_equal "United States of America", result.country
+    assert_equal([37.805402, -122.272797], result.coordinates)
+  end
+
+  def test_low_accuracy
+    result = Geocoder.search("low accuracy").first
+    assert_equal "United States of America", result.country
+  end
+
+  def test_raises_api_key_exception
+    Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid key")
+    end
+  end
+end
diff --git a/test/unit/lookups/nationaal_georegister_nl_test.rb b/test/unit/lookups/nationaal_georegister_nl_test.rb
new file mode 100644
index 0000000..4c17fb9
--- /dev/null
+++ b/test/unit/lookups/nationaal_georegister_nl_test.rb
@@ -0,0 +1,25 @@
+# encoding: utf-8
+require 'test_helper'
+
+class NationaalGeoregisterNlTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :nationaal_georegister_nl)
+  end
+
+  def test_result_components
+    result = Geocoder.search('Nieuwezijds Voorburgwal 147, Amsterdam').first
+
+    assert_equal result.street,         'Nieuwezijds Voorburgwal'
+    assert_equal result.street_number,  '147'
+    assert_equal result.city,           'Amsterdam'
+    assert_equal result.postal_code,    '1012RJ'
+    assert_equal result.address,        'Nieuwezijds Voorburgwal 147, 1012RJ Amsterdam'
+    assert_equal result.province,       'Noord-Holland'
+    assert_equal result.province_code,  'PV27'
+    assert_equal result.country_code,   'NL'
+    assert_equal result.latitude,       52.37316397
+    assert_equal result.longitude,      4.89089949
+  end
+end
diff --git a/test/unit/lookups/nominatim_test.rb b/test/unit/lookups/nominatim_test.rb
new file mode 100644
index 0000000..f4a6fa4
--- /dev/null
+++ b/test/unit/lookups/nominatim_test.rb
@@ -0,0 +1,51 @@
+# encoding: utf-8
+require 'test_helper'
+
+class NominatimTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :nominatim)
+    set_api_key!(:nominatim)
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "10001", result.postal_code
+    assert_nil result.city_district
+    assert_nil result.state_district
+    assert_nil result.neighbourhood
+    assert_equal "Madison Square Garden, West 31st Street, Long Island City, New York City, New York, 10001, United States of America", result.address
+  end
+
+  def test_result_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.749828338623, -73.9943389892578, 40.7511596679688, -73.9926528930664],
+      result.viewport
+  end
+
+  def test_city_state_district
+    result = Geocoder.search("cologne cathedral cologne germany").first
+    assert_equal "Innenstadt", result.city_district
+    assert_equal "Cologne Government Region", result.state_district
+  end
+
+  def test_neighbourhood
+    result = Geocoder.search("cologne cathedral cologne germany").first
+    assert_equal "Kunibertsviertel", result.neighbourhood
+  end
+
+  def test_host_configuration
+    Geocoder.configure(nominatim: {host: "local.com"})
+    query = Geocoder::Query.new("Bluffton, SC")
+    assert_match %r(http://local\.com), query.url
+  end
+
+  def test_raises_exception_when_over_query_limit
+    Geocoder.configure(:always_raise => [Geocoder::OverQueryLimitError])
+    l = Geocoder::Lookup.get(:nominatim)
+    assert_raises Geocoder::OverQueryLimitError do
+      l.send(:results, Geocoder::Query.new("over limit"))
+    end
+  end
+end
diff --git a/test/unit/lookups/opencagedata_test.rb b/test/unit/lookups/opencagedata_test.rb
new file mode 100644
index 0000000..b0fdc6e
--- /dev/null
+++ b/test/unit/lookups/opencagedata_test.rb
@@ -0,0 +1,91 @@
+# encoding: utf-8
+require 'test_helper'
+
+class OpencagedataTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :opencagedata)
+    set_api_key!(:opencagedata)
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "West 31st Street", result.street
+    assert_match(/46, West 31st Street, Koreatown, New York County, 10011, New York City, New York, United States of America/, result.address)
+
+  end
+
+  def test_opencagedata_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.7498531, -73.9944444, 40.751161, -73.9925922],
+      result.viewport
+  end
+
+  def test_opencagedata_query_url_contains_bounds
+    lookup = Geocoder::Lookup::Opencagedata.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some street",
+      :bounds => [[40.0, -120.0], [39.0, -121.0]]
+    ))
+    assert_match(/bounds=40.0+%2C-120.0+%2C39.0+%2C-121.0+/, url)
+  end
+
+  def test_opencagedata_query_url_contains_optional_params
+    lookup = Geocoder::Lookup::Opencagedata.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some street",
+      :countrycode => 'fr',
+      :min_confidence => 5,
+      :no_dedupe => 1,
+      :no_annotations => 1,
+      :no_record => 1,
+      :limit => 2
+    ))
+    assert_match(/countrycode=fr/, url)
+    assert_match(/min_confidence=5/, url)
+    assert_match(/no_dedupe=1/, url)
+    assert_match(/no_annotations=1/, url)
+    assert_match(/no_record=1/, url)
+    assert_match(/limit=2/, url)
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+
+  def test_opencagedata_reverse_url
+    query = Geocoder::Query.new([45.423733, -75.676333])
+    assert_match(/\bq=45.423733%2C-75.676333\b/, query.url)
+  end
+
+  def test_opencagedata_time_zone
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "America/New_York", result.time_zone
+  end
+
+  def test_raises_exception_when_invalid_request
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search("invalid request")
+    end
+  end
+
+  def test_raises_exception_when_invalid_api_key
+    Geocoder.configure(always_raise: [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid api key")
+    end
+  end
+
+
+  def test_raises_exception_when_over_query_limit
+    Geocoder.configure(:always_raise => [Geocoder::OverQueryLimitError])
+    l = Geocoder::Lookup.get(:opencagedata)
+    assert_raises Geocoder::OverQueryLimitError do
+      l.send(:results, Geocoder::Query.new("over limit"))
+    end
+  end
+end
diff --git a/test/unit/lookups/osmnames_test.rb b/test/unit/lookups/osmnames_test.rb
new file mode 100644
index 0000000..5816b45
--- /dev/null
+++ b/test/unit/lookups/osmnames_test.rb
@@ -0,0 +1,65 @@
+# encoding: utf-8
+require 'test_helper'
+
+class OsmnamesTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :osmnames)
+    set_api_key!(:osmnames)
+  end
+
+  def test_url_contains_api_key
+    Geocoder.configure(osmnames: {api_key: 'abc123'})
+    query = Geocoder::Query.new('test')
+    assert_includes query.url, 'key=abc123'
+  end
+
+  def test_url_contains_query_base
+    query = Geocoder::Query.new("Madison Square Garden, New York, NY")
+    assert_includes query.url, 'geocoder.tilehosting.com/q/Madison'
+  end
+
+  def test_url_contains_country_code
+    query = Geocoder::Query.new("test", country_code: 'US')
+    assert_includes query.url, 'https://geocoder.tilehosting.com/us/q/'
+  end
+
+  def test_result_components
+    result = Geocoder.search('Madison Square Garden, New York, NY').first
+    assert_equal [40.693073, -73.878418], result.coordinates
+    assert_equal 'New York City, New York, United States of America', result.address
+    assert_equal 'New York', result.state
+    assert_equal 'New York City', result.city
+    assert_equal 'us', result.country_code
+  end
+
+  def test_result_for_reverse_geocode
+    result = Geocoder.search('-73.878418, 40.693073').first
+    assert_equal 'New York City, New York, United States of America', result.address
+    assert_equal 'New York', result.state
+    assert_equal 'New York City', result.city
+    assert_equal 'us', result.country_code
+  end
+
+  def test_url_for_reverse_geocode
+    query = Geocoder::Query.new("-73.878418, 40.693073")
+    assert_includes query.url, 'https://geocoder.tilehosting.com/r/-73.878418/40.693073.js'
+  end
+
+  def test_result_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.477398, -74.259087, 40.91618, -73.70018], result.viewport
+  end
+
+  def test_no_results
+    assert_equal [], Geocoder.search("no results")
+  end
+
+  def test_raises_exception_when_return_message_error
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest.new("Invalid attribute value.") do
+      Geocoder.search("invalid request")
+    end
+  end
+end
diff --git a/test/unit/lookups/pelias_test.rb b/test/unit/lookups/pelias_test.rb
new file mode 100644
index 0000000..d2304b7
--- /dev/null
+++ b/test/unit/lookups/pelias_test.rb
@@ -0,0 +1,27 @@
+# encoding: utf-8
+require 'test_helper'
+
+class PeliasTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :pelias, api_key: 'abc123', pelias: {}) # Empty pelias hash only for test (pollution control)
+  end
+
+  def test_configure_default_endpoint
+    query = Geocoder::Query.new('Madison Square Garden, New York, NY')
+    assert_true query.url.start_with?('http://localhost/v1/search'), query.url
+  end
+
+  def test_configure_custom_endpoint
+    Geocoder.configure(lookup: :pelias, api_key: 'abc123', pelias: {endpoint: 'self.hosted.pelias/proxy'})
+    query = Geocoder::Query.new('Madison Square Garden, New York, NY')
+    assert_true query.url.start_with?('http://self.hosted.pelias/proxy/v1/search'), query.url
+  end
+
+  def test_query_for_reverse_geocode
+    lookup = Geocoder::Lookup::Pelias.new
+    url = lookup.query_url(Geocoder::Query.new([45.423733, -75.676333]))
+    assert_match(/point.lat=45.423733&point.lon=-75.676333/, url)
+  end
+end
diff --git a/test/unit/lookups/photon_test.rb b/test/unit/lookups/photon_test.rb
new file mode 100644
index 0000000..3e426ca
--- /dev/null
+++ b/test/unit/lookups/photon_test.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+# Test for Photon
+class PhotonTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :photon)
+  end
+
+  def test_photon_forward_geocoding_result_properties
+    result = Geocoder.search('Madison Square Garden, New York, NY').first
+
+    geometry = { type: 'Point', coordinates: [-73.99355027800776, 40.7505247] }
+    bounds = [-73.9944446, 40.751161, -73.9925924, 40.7498531]
+
+    assert_equal(40.7505247, result.latitude)
+    assert_equal(-73.99355027800776, result.longitude)
+    assert_equal '4 Pennsylvania Plaza', result.street_address
+    assert_equal 'Madison Square Garden, 4 Pennsylvania Plaza, New York, New York, 10001, United States of America',
+                 result.address
+    assert_equal '4', result.house_number
+    assert_equal 'Pennsylvania Plaza', result.street
+    assert_equal geometry, result.geometry
+    assert_equal bounds, result.bounds
+    assert_equal :way, result.type
+    assert_equal 138_141_251, result.osm_id
+    assert_equal 'leisure=stadium', result.osm_tag
+  end
+
+  def test_photon_reverse_geocoding_result_properties
+    result = Geocoder.search([45.423733, -75.676333]).first
+
+    geometry = { type: 'Point', coordinates: [-73.9935078, 40.750499] }
+
+    assert_equal(40.750499, result.latitude)
+    assert_equal(-73.9935078, result.longitude)
+    assert_equal '4 Pennsylvania Plaza', result.street_address
+    assert_equal '4 Pennsylvania Plaza, New York, New York, 10121, United States of America',
+                 result.address
+    assert_equal '4', result.house_number
+    assert_equal 'Pennsylvania Plaza', result.street
+    assert_equal geometry, result.geometry
+    assert_nil result.bounds
+    assert_equal :node, result.type
+    assert_equal 6_985_936_386, result.osm_id
+    assert_equal 'tourism=attraction', result.osm_tag
+  end
+
+  def test_photon_query_url_contains_language
+    lookup = Geocoder::Lookup::Photon.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        language: 'de'
+      )
+    )
+    assert_match(/lang=de/, url)
+  end
+
+  def test_photon_query_url_contains_limit
+    lookup = Geocoder::Lookup::Photon.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        limit: 5
+      )
+    )
+    assert_match(/limit=5/, url)
+  end
+
+  def test_photon_query_url_contains_query
+    lookup = Geocoder::Lookup::Photon.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query'
+      )
+    )
+    assert_match(/q=Test\+Query/, url)
+  end
+
+  def test_photon_query_url_contains_params
+    lookup = Geocoder::Lookup::Photon.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        'Test Query',
+        bias: {
+          latitude: 45.423733,
+          longitude: -75.676333,
+          scale: 4
+        },
+        filter: {
+          bbox: [-73.9944446, 40.751161, -73.9925924, 40.7498531],
+          osm_tag: 'leisure:stadium'
+        }
+      )
+    )
+    assert_match(/q=Test\+Query/, url)
+    assert_match(/lat=45\.423733/, url)
+    assert_match(/lon=-75\.676333/, url)
+    assert_match(/bbox=-73\.9944446%2C40\.751161%2C-73\.9925924%2C40\.7498531/, url)
+    assert_match(/osm_tag=leisure%3Astadium/, url)
+  end
+
+  def test_photon_reverse_query_url_contains_lat_lon
+    lookup = Geocoder::Lookup::Photon.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        [45.423733, -75.676333]
+      )
+    )
+    assert_no_match(/q=.*/, url)
+    assert_match(/lat=45\.423733/, url)
+    assert_match(/lon=-75\.676333/, url)
+  end
+
+  def test_photon_reverse_query_url_contains_params
+    lookup = Geocoder::Lookup::Photon.new
+    url = lookup.query_url(
+      Geocoder::Query.new(
+        [45.423733, -75.676333],
+        radius: 5,
+        distance_sort: true,
+        filter: {
+          string: 'query string filter'
+        }
+      )
+    )
+    assert_no_match(/q=.*/, url)
+    assert_match(/lat=45\.423733/, url)
+    assert_match(/lon=-75\.676333/, url)
+    assert_match(/radius=5/, url)
+    assert_match(/distance_sort=true/, url)
+    assert_match(/query_string_filter=query\+string\+filter/, url)
+  end
+
+  def test_photon_invalid_request
+    Geocoder.configure(always_raise: [Geocoder::InvalidRequest])
+    assert_raises Geocoder::InvalidRequest do
+      Geocoder.search('invalid request')
+    end
+  end
+end
diff --git a/test/unit/lookups/pickpoint_test.rb b/test/unit/lookups/pickpoint_test.rb
new file mode 100644
index 0000000..b9bbf6d
--- /dev/null
+++ b/test/unit/lookups/pickpoint_test.rb
@@ -0,0 +1,34 @@
+require 'test_helper'
+
+class PickpointTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :pickpoint)
+    set_api_key!(:pickpoint)
+  end
+
+  def test_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "10001", result.postal_code
+    assert_equal "Madison Square Garden, West 31st Street, Long Island City, New York City, New York, 10001, United States of America", result.address
+  end
+
+  def test_result_viewport
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal [40.749828338623, -73.9943389892578, 40.7511596679688, -73.9926528930664], result.viewport
+  end
+
+  def test_url_contains_api_key
+    Geocoder.configure(pickpoint: {api_key: "pickpoint-api-key"})
+    query = Geocoder::Query.new("Leadville, CO")
+    assert_match(/key=pickpoint-api-key/, query.url)
+  end
+
+  def test_raises_exception_with_invalid_api_key
+    Geocoder.configure(always_raise: [Geocoder::InvalidApiKey])
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search("invalid api key")
+    end
+  end
+end
diff --git a/test/unit/lookups/pointpin_test.rb b/test/unit/lookups/pointpin_test.rb
new file mode 100644
index 0000000..8dfe2ab
--- /dev/null
+++ b/test/unit/lookups/pointpin_test.rb
@@ -0,0 +1,44 @@
+# encoding: utf-8
+require 'test_helper'
+
+class PointpinTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :pointpin, api_key: "abc123")
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("80.111.55.55").first
+    assert result.is_a?(Geocoder::Result::Pointpin)
+  end
+
+  def test_result_on_loopback_ip_address_search
+    results = Geocoder.search("127.0.0.1")
+    assert_equal 0, results.length
+  end
+
+  def test_result_on_private_ip_address_search
+    results = Geocoder.search("172.19.0.1")
+    assert_equal 0, results.length
+  end
+
+  def test_result_components
+    result = Geocoder.search("80.111.55.55").first
+    assert_equal "Dublin, Dublin City, 8, Ireland", result.address
+  end
+
+  def test_no_results
+    silence_warnings do
+      results = Geocoder.search("8.8.8.8")
+      assert_equal 0, results.length
+    end
+  end
+
+  def test_invalid_address
+    silence_warnings do
+      results = Geocoder.search("555.555.555.555", ip_address: true)
+      assert_equal 0, results.length
+    end
+  end
+end
diff --git a/test/unit/lookups/postcode_anywhere_uk_test.rb b/test/unit/lookups/postcode_anywhere_uk_test.rb
new file mode 100644
index 0000000..a1b7a23
--- /dev/null
+++ b/test/unit/lookups/postcode_anywhere_uk_test.rb
@@ -0,0 +1,70 @@
+# encoding: utf-8
+require 'test_helper'
+
+class PostcodeAnywhereUkTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :postcode_anywhere_uk)
+    set_api_key!(:postcode_anywhere_uk)
+  end
+
+  def test_result_components_with_placename_search
+    results = Geocoder.search('Romsey')
+
+    assert_equal 1, results.size
+    assert_equal 'Romsey, Hampshire', results.first.address
+    assert_equal 'SU 35270 21182', results.first.os_grid
+    assert_equal [50.9889, -1.4989], results.first.coordinates
+    assert_equal 'Romsey', results.first.city
+  end
+
+  def test_result_components_with_postcode
+    results = Geocoder.search('WR26NJ')
+
+    assert_equal 1, results.size
+    assert_equal 'Moseley Road, Hallow, Worcester', results.first.address
+    assert_equal 'SO 81676 59425', results.first.os_grid
+    assert_equal [52.2327, -2.2697], results.first.coordinates
+    assert_equal 'Hallow', results.first.city
+  end
+
+  def test_result_components_with_county
+    results = Geocoder.search('hampshire')
+
+    assert_equal 1, results.size
+    assert_equal 'Hampshire', results.first.address
+    assert_equal 'SU 48701 26642', results.first.os_grid
+    assert_equal [51.037, -1.3068], results.first.coordinates
+    assert_equal '', results.first.city
+  end
+
+  def test_no_results
+    assert_equal [], Geocoder.search('no results')
+  end
+
+  def test_key_limit_exceeded_error
+    Geocoder.configure(always_raise: [Geocoder::OverQueryLimitError])
+
+    assert_raises Geocoder::OverQueryLimitError do
+      Geocoder.search('key limit exceeded')
+    end
+  end
+
+  def test_unknown_key_error
+    Geocoder.configure(always_raise: [Geocoder::InvalidApiKey])
+
+    assert_raises Geocoder::InvalidApiKey do
+      Geocoder.search('unknown key')
+    end
+  end
+
+  def test_generic_error
+    Geocoder.configure(always_raise: [Geocoder::Error])
+
+    exception = assert_raises(Geocoder::Error) do
+      Geocoder.search('generic error')
+    end
+    assert_equal 'A generic error occured.', exception.message
+  end
+end
diff --git a/test/unit/lookups/postcodes_io_test.rb b/test/unit/lookups/postcodes_io_test.rb
new file mode 100644
index 0000000..6571f64
--- /dev/null
+++ b/test/unit/lookups/postcodes_io_test.rb
@@ -0,0 +1,22 @@
+# encoding: utf-8
+require 'test_helper'
+
+class PostcodesIoTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :postcodes_io)
+  end
+
+  def test_result_on_postcode_search
+    results = Geocoder.search('WR26NJ')
+
+    assert_equal 1, results.size
+    assert_equal 'Worcestershire', results.first.county
+    assert_equal [52.2327158260535, -2.26972239639173], results.first.coordinates
+  end
+
+  def test_no_results
+    assert_equal [], Geocoder.search('no results')
+  end
+end
diff --git a/test/unit/lookups/smarty_streets_test.rb b/test/unit/lookups/smarty_streets_test.rb
new file mode 100644
index 0000000..853b0e3
--- /dev/null
+++ b/test/unit/lookups/smarty_streets_test.rb
@@ -0,0 +1,98 @@
+# encoding: utf-8
+require 'test_helper'
+
+class SmartyStreetsTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :smarty_streets)
+    set_api_key!(:smarty_streets)
+  end
+
+  def test_url_contains_api_key
+    Geocoder.configure(:smarty_streets => {:api_key => 'blah'})
+    query = Geocoder::Query.new("Bluffton, SC")
+    assert_match(/auth-token=blah/, query.url)
+  end
+
+  def test_query_for_address_geocode
+    query = Geocoder::Query.new("42 Wallaby Way Sydney, AU")
+    assert_match(/api\.smartystreets\.com\/street-address\?/, query.url)
+  end
+
+  def test_query_for_zipcode_geocode
+    query = Geocoder::Query.new("22204")
+    assert_match(/us-zipcode\.api\.smartystreets\.com\/lookup\?/, query.url)
+  end
+
+  def test_query_for_zipfour_geocode
+    query = Geocoder::Query.new("22204-1603")
+    assert_match(/us-zipcode\.api\.smartystreets\.com\/lookup\?/, query.url)
+  end
+
+  def test_query_for_international_geocode
+    query = Geocoder::Query.new("13 rue yves toudic 75010", country: "France")
+    assert_match(/international-street\.api\.smartystreets\.com\/verify\?/, query.url)
+  end
+
+  def test_smarty_streets_result_components
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "Penn", result.street
+    assert_equal "10121", result.zipcode
+    assert_equal "1703", result.zip4
+    assert_equal "New York", result.city
+    assert_equal "36061", result.fips
+    assert_equal "US", result.country_code
+    assert !result.zipcode_endpoint?
+  end
+
+  def test_smarty_streets_result_components_with_zipcode_only_query
+    result = Geocoder.search("11211").first
+    assert_equal "Brooklyn", result.city
+    assert_equal "New York", result.state
+    assert_equal "NY", result.state_code
+    assert_equal "US", result.country_code
+    assert result.zipcode_endpoint?
+  end
+
+  def test_smarty_streets_result_components_with_international_query
+    result = Geocoder.search("13 rue yves toudic 75010", country: "France").first
+    assert_equal 'Yves Toudic', result.street
+    assert_equal 'Paris', result.city
+    assert_equal '75010', result.postal_code
+    assert_equal 'FRA', result.country_code
+    assert result.international_endpoint?
+  end
+
+  def test_smarty_streets_when_longitude_latitude_does_not_exist
+    result = Geocoder.search("96628").first
+    assert_equal nil, result.coordinates
+  end
+
+  def test_no_results
+    results = Geocoder.search("no results")
+    assert_equal 0, results.length
+  end
+
+  def test_invalid_zipcode_returns_no_results
+    assert_nothing_raised do
+      assert_nil Geocoder.search("10300").first
+    end
+  end
+
+  def test_raises_exception_on_error_http_status
+    error_statuses = {
+      '400' => Geocoder::InvalidRequest,
+      '401' => Geocoder::RequestDenied,
+      '402' => Geocoder::OverQueryLimitError
+    }
+    Geocoder.configure(always_raise: error_statuses.values)
+    lookup = Geocoder::Lookup.get(:smarty_streets)
+    error_statuses.each do |code, err|
+      assert_raises err do
+        response = MockHttpResponse.new(code: code.to_i)
+        lookup.send(:check_response_for_errors!, response)
+      end
+    end
+  end
+end
diff --git a/test/unit/lookups/telize_test.rb b/test/unit/lookups/telize_test.rb
new file mode 100644
index 0000000..f8e87ab
--- /dev/null
+++ b/test/unit/lookups/telize_test.rb
@@ -0,0 +1,91 @@
+# encoding: utf-8
+require 'test_helper'
+
+class TelizeTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(ip_lookup: :telize, telize: {host: nil})
+    set_api_key!(:telize)
+  end
+
+  def test_query_url
+    lookup = Geocoder::Lookup::Telize.new
+    query = Geocoder::Query.new("74.200.247.59")
+    assert_match %r{^https://telize-v1\.p\.rapidapi\.com/location/74\.200\.247\.59}, lookup.query_url(query)
+  end
+
+  def test_includes_api_key_when_set
+    Geocoder.configure(api_key: "api_key")
+    lookup = Geocoder::Lookup::Telize.new
+    query = Geocoder::Query.new("74.200.247.59")
+    assert_match %r{/location/74\.200\.247\.59\?rapidapi-key=api_key}, lookup.query_url(query)
+  end
+
+  def test_uses_custom_host_when_set
+    Geocoder.configure(telize: {host: "example.com"})
+    lookup = Geocoder::Lookup::Telize.new
+    query = Geocoder::Query.new("74.200.247.59")
+    assert_match %r{^http://example\.com/location/74\.200\.247\.59$}, lookup.query_url(query)
+  end
+
+  def test_allows_https_when_custom_host
+    Geocoder.configure(use_https: true, telize: {host: "example.com"})
+    lookup = Geocoder::Lookup::Telize.new
+    query = Geocoder::Query.new("74.200.247.59")
+    assert_match %r{^https://example\.com}, lookup.query_url(query)
+  end
+
+  def test_requires_https_when_not_custom_host
+    Geocoder.configure(use_https: false)
+    lookup = Geocoder::Lookup::Telize.new
+    query = Geocoder::Query.new("74.200.247.59")
+    assert_match %r{^https://telize-v1\.p\.rapidapi\.com}, lookup.query_url(query)
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::Telize)
+  end
+
+  def test_result_on_loopback_ip_address_search
+    result = Geocoder.search("127.0.0.1").first
+    assert_equal "127.0.0.1", result.ip
+    assert_equal '',          result.country_code
+    assert_equal '',          result.country
+  end
+
+  def test_result_on_private_ip_address_search
+    result = Geocoder.search("172.19.0.1").first
+    assert_equal "172.19.0.1", result.ip
+    assert_equal '',           result.country_code
+    assert_equal '',           result.country
+  end
+
+  def test_result_components
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Jersey City, NJ 07302, United States", result.address
+    assert_equal "US", result.country_code
+    assert_equal [40.7209, -74.0468], result.coordinates
+  end
+
+  def test_no_results
+    results = Geocoder.search("8.8.8.8")
+    assert_equal 0, results.length
+  end
+
+  def test_invalid_address
+    results = Geocoder.search("555.555.555.555", ip_address: true)
+    assert_equal 0, results.length
+  end
+
+  def test_cache_key_strips_off_query_string
+    Geocoder.configure(telize: {api_key: "xxxxx"})
+    lookup = Geocoder::Lookup.get(:telize)
+    query = Geocoder::Query.new("8.8.8.8")
+    qurl = lookup.send(:query_url, query)
+    key = lookup.send(:cache_key, query)
+    assert qurl.include?("rapidapi-key")
+    assert !key.include?("rapidapi-key")
+  end
+end
diff --git a/test/unit/lookups/twogis_test.rb b/test/unit/lookups/twogis_test.rb
new file mode 100644
index 0000000..29729c4
--- /dev/null
+++ b/test/unit/lookups/twogis_test.rb
@@ -0,0 +1,103 @@
+# encoding: utf-8
+require 'test_helper'
+
+class TwogisTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :twogis)
+    set_api_key!(:twogis)
+  end
+
+  def test_twogis_point
+    result = Geocoder.search('Kremlin, Moscow, Russia').first
+    assert_equal [55.755836, 37.617774], result.coordinates
+  end
+
+  def test_twogis_no_results
+    silence_warnings do
+      results = Geocoder.search("no results")
+      assert_equal 0, results.length
+    end
+  end
+
+  def test_twogis_no_city
+    result = Geocoder.search('chernoe more').first
+    assert_equal "", result.city
+  end
+
+  def test_twogis_no_country
+    result = Geocoder.search('new york').first
+    assert_equal "", result.country
+  end
+
+  def test_twogis_result_kind
+    assert_nothing_raised do
+      ["new york", [55.755836, 37.617774], 'chernoe more'].each do |query|
+        Geocoder.search(query).first.type
+      end
+    end
+  end
+
+  def test_twogis_result_returns_street_name
+    assert_nothing_raised do
+      result = Geocoder.search("ohotniy riad 2").first
+      assert_equal "улица Охотный Ряд", result.street
+    end
+  end
+
+  def test_twogis_result_returns_street_address
+    assert_nothing_raised do
+      result = Geocoder.search("ohotniy riad 2").first
+      assert_equal "улица Охотный Ряд, 2", result.street_address
+    end
+  end
+
+  def test_twogis_result_returns_street_number
+    assert_nothing_raised do
+      result = Geocoder.search("ohotniy riad 2").first
+      assert_equal "2", result.street_number
+    end
+  end
+
+  def test_twogis_maximum_precision_on_russian_address
+    result = Geocoder.search('ohotniy riad 2').first
+
+    assert_equal [55.757261, 37.616732], result.coordinates
+
+    assert_equal "Москва, улица Охотный Ряд, 2",
+                 result.address
+    assert_equal "Тверской район", result.district
+    assert_equal "Москва", result.city
+    assert_equal "Москва", result.region
+    assert_equal "Россия", result.country
+    assert_equal "улица Охотный Ряд, 2", result.street_address
+    assert_equal "улица Охотный Ряд", result.street
+    assert_equal "2", result.street_number
+
+    assert_equal "building", result.type
+    assert_equal "Многофункциональный комплекс", result.purpose_name
+    assert_equal "Four Seasons Moscow, отель", result.building_name
+  end
+
+  def test_twogis_hydro_object
+    result = Geocoder.search('volga river').first
+
+    assert_equal [57.953151, 38.388873], result.coordinates
+    assert_equal "", result.address
+    assert_equal "", result.district
+    assert_equal "Некоузский район", result.district_area
+    assert_equal "Россия", result.country
+    assert_equal "Ярославская область", result.region
+    assert_equal "", result.street_address
+    assert_equal "", result.street
+    assert_equal "", result.street_number
+    assert_equal "adm_div", result.type
+    assert_equal "", result.purpose_name
+    assert_equal "", result.building_name
+    assert_equal "settlement", result.subtype
+    assert_equal "Посёлок", result.subtype_specification
+    assert_equal "settlement", result.subtype
+    assert_equal "Волга", result.name
+  end
+end
diff --git a/test/unit/lookups/uk_ordnance_survey_names.rb b/test/unit/lookups/uk_ordnance_survey_names.rb
new file mode 100644
index 0000000..5ed2648
--- /dev/null
+++ b/test/unit/lookups/uk_ordnance_survey_names.rb
@@ -0,0 +1,23 @@
+# encoding: utf-8
+require 'test_helper'
+
+class UkOrdnanceSurveyNamesTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :uk_ordnance_survey_names)
+    set_api_key!(:uk_ordnance_survey_names)
+  end
+
+  def test_result_on_placename_search
+    result = Geocoder.search('London').first
+    assert_in_delta 51.51437, result.coordinates[0]
+    assert_in_delta -0.09227, result.coordinates[1]
+  end
+
+  def test_result_on_postcode_search
+    result = Geocoder.search('SW1A1AA').first
+    assert_in_delta 51.50100, result.coordinates[0]
+    assert_in_delta -0.14157, result.coordinates[1]
+  end
+end
diff --git a/test/unit/lookups/yandex_test.rb b/test/unit/lookups/yandex_test.rb
new file mode 100644
index 0000000..dcd2896
--- /dev/null
+++ b/test/unit/lookups/yandex_test.rb
@@ -0,0 +1,176 @@
+# encoding: utf-8
+$: << File.join(File.dirname(__FILE__), "..", "..")
+require 'test_helper'
+
+class YandexTest < GeocoderTestCase
+
+  def setup
+    super
+    Geocoder.configure(lookup: :yandex, language: :en)
+  end
+
+  def test_yandex_viewport
+    result = Geocoder.search('Kremlin, Moscow, Russia').first
+    assert_equal [55.748189, 37.612587, 55.755044, 37.623187],
+      result.viewport
+  end
+
+  def test_yandex_no_country_in_results
+    result = Geocoder.search('black sea').first
+    assert_equal "", result.country_code
+    assert_equal "", result.country
+  end
+
+  def test_yandex_query_url_contains_bbox
+    lookup = Geocoder::Lookup::Yandex.new
+    url = lookup.query_url(Geocoder::Query.new(
+      "Some Intersection",
+      :bounds => [[40.0, -120.0], [39.0, -121.0]]
+    ))
+    if RUBY_VERSION < '2.5.0'
+      assert_match(/bbox=40.0+%2C-120.0+%7E39.0+%2C-121.0+/, url)
+    else
+      assert_match(/bbox=40.0+%2C-120.0+~39.0+%2C-121.0+/, url)
+    end
+  end
+
+  def test_yandex_result_without_city_does_not_raise_exception
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      result = Geocoder.search("no city and town").first
+      assert_equal "", result.city
+    end
+  end
+
+  def test_yandex_result_without_admin_area_no_exception
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      result = Geocoder.search("no administrative area").first
+      assert_equal "", result.city
+    end
+  end
+
+  def test_yandex_result_new_york
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      result = Geocoder.search("new york").first
+      assert_equal "", result.city
+    end
+  end
+
+  def test_yandex_result_kind
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      ["new york", [45.423733, -75.676333], "no city and town"].each do |query|
+        Geocoder.search("new york").first.kind
+      end
+    end
+  end
+
+  def test_yandex_result_without_locality_name
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      result = Geocoder.search("canada rue dupuis 14")[6]
+      assert_equal "", result.city
+    end
+  end
+
+  def test_yandex_result_returns_street_name
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      result = Geocoder.search("canada rue dupuis 14")[6]
+      assert_equal "Rue Hormidas-Dupuis", result.street
+    end
+  end
+
+  def test_yandex_result_returns_street_number
+    assert_nothing_raised do
+      set_api_key!(:yandex)
+      result = Geocoder.search("canada rue dupuis 14")[6]
+      assert_equal "14", result.street_number
+    end
+  end
+
+  def test_yandex_find_in_hash_method
+    result = Geocoder::Result::Yandex.new({})
+    hash = {
+      'root_node' => {
+        'node_1' => [1, 2, 3],
+        'node_2' => {
+          'data' => 'foo'
+        }
+      }
+    }
+
+    assert_equal [1, 2, 3], result.send(:find_in_hash, hash, 'root_node', 'node_1')
+    assert_equal "foo", result.send(:find_in_hash, hash, 'root_node', 'node_2', 'data')
+    assert_equal nil, result.send(:find_in_hash, hash, 'root_node', 'node_3')
+    assert_equal nil, result.send(:find_in_hash, hash, 'root_node', 'node_2', 'another_data')
+    assert_equal nil, result.send(:find_in_hash, hash, 'root_node', 'node_2', 'data', 'x')
+  end
+
+  def test_yandex_maximum_precision_on_russian_address
+    result = Geocoder.search('putilkovo novotushinskaya 5').first
+
+    assert_equal [55.872258, 37.403522], result.coordinates
+    assert_equal [55.86995, 37.399416, 55.874567, 37.407627], result.viewport
+
+    assert_equal "Russia, Moscow Region, gorodskoy okrug Krasnogorsk, " \
+                 "derevnya Putilkovo, Novotushinskaya ulitsa, 5",
+                 result.address
+    assert_equal "derevnya Putilkovo", result.city
+    assert_equal "Russia", result.country
+    assert_equal "RU", result.country_code
+    assert_equal "Moscow Region", result.state
+    assert_equal "gorodskoy okrug Krasnogorsk", result.sub_state
+    assert_equal "", result.state_code
+    assert_equal "Novotushinskaya ulitsa", result.street
+    assert_equal "5", result.street_number
+    assert_equal "", result.premise_name
+    assert_equal "143441", result.postal_code
+    assert_equal "house", result.kind
+    assert_equal "exact", result.precision
+  end
+
+  def test_yandex_hydro_object
+    result = Geocoder.search('volga river').first
+
+    assert_equal [49.550996, 45.139984], result.coordinates
+    assert_equal [45.697053, 32.468241, 58.194645, 50.181608], result.viewport
+
+    assert_equal "Russia, Volga River", result.address
+    assert_equal "", result.city
+    assert_equal "Russia", result.country
+    assert_equal "RU", result.country_code
+    assert_equal "", result.state
+    assert_equal "", result.sub_state
+    assert_equal "", result.state_code
+    assert_equal "", result.street
+    assert_equal "", result.street_number
+    assert_equal "Volga River", result.premise_name
+    assert_equal "", result.postal_code
+    assert_equal "hydro", result.kind
+    assert_equal "other", result.precision
+  end
+
+  def test_yandex_province_object
+    result = Geocoder.search('ontario').first
+
+    assert_equal [49.294248, -87.170557], result.coordinates
+    assert_equal [41.704494, -95.153382, 56.88699, -74.321387], result.viewport
+
+    assert_equal "Canada, Ontario", result.address
+    assert_equal "", result.city
+    assert_equal "Canada", result.country
+    assert_equal "CA", result.country_code
+    assert_equal "Ontario", result.state
+    assert_equal "", result.sub_state
+    assert_equal "", result.state_code
+    assert_equal "", result.street
+    assert_equal "", result.street_number
+    assert_equal "", result.premise_name
+    assert_equal "", result.postal_code
+    assert_equal "province", result.kind
+    assert_equal "other", result.precision
+  end
+end
diff --git a/test/unit/method_aliases_test.rb b/test/unit/method_aliases_test.rb
new file mode 100644
index 0000000..c0a0ba7
--- /dev/null
+++ b/test/unit/method_aliases_test.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+require 'test_helper'
+
+class MethodAliasesTest < GeocoderTestCase
+
+  def test_distance_from_is_alias_for_distance_to
+    v = Place.new(*geocoded_object_params(:msg))
+    v.latitude, v.longitude = [40.750354, -73.993371]
+    assert_equal v.distance_from([30, -94]), v.distance_to([30, -94])
+  end
+
+  def test_fetch_coordinates_is_alias_for_geocode
+    v = Place.new(*geocoded_object_params(:msg))
+    assert_equal [Float, Float], v.fetch_coordinates.map(&:class)
+  end
+
+  def test_fetch_address_is_alias_for_reverse_geocode
+    v = PlaceReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    assert_match(/New York/, v.fetch_address)
+  end
+end
diff --git a/test/unit/model_test.rb b/test/unit/model_test.rb
new file mode 100644
index 0000000..39612fd
--- /dev/null
+++ b/test/unit/model_test.rb
@@ -0,0 +1,42 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ModelTest < GeocoderTestCase
+
+  def test_geocode_with_block_runs_block
+    e = PlaceWithCustomResultsHandling.new(*geocoded_object_params(:msg))
+    e.geocode
+    assert_match(/[0-9\.,\-]+/, e.coords_string)
+  end
+
+  def test_geocode_with_block_doesnt_auto_assign_coordinates
+    e = PlaceWithCustomResultsHandling.new(*geocoded_object_params(:msg))
+    e.geocode
+    assert_nil e.latitude
+    assert_nil e.longitude
+  end
+
+  def test_reverse_geocode_with_block_runs_block
+    e = PlaceReverseGeocodedWithCustomResultsHandling.new(*reverse_geocoded_object_params(:msg))
+    e.reverse_geocode
+    assert_equal "US", e.country.upcase
+  end
+
+  def test_reverse_geocode_with_block_doesnt_auto_assign_address
+    e = PlaceReverseGeocodedWithCustomResultsHandling.new(*reverse_geocoded_object_params(:msg))
+    e.reverse_geocode
+    assert_nil e.address
+  end
+
+  def test_units_and_method
+    PlaceReverseGeocoded.reverse_geocoded_by :latitude, :longitude, method: :spherical, units: :km
+    assert_equal :km,        PlaceReverseGeocoded.geocoder_options[:units]
+    assert_equal :spherical, PlaceReverseGeocoded.geocoder_options[:method]
+  end
+
+  def test_params
+    params = {incude: "cios2"}
+    PlaceReverseGeocoded.reverse_geocoded_by :latitude, :longitude, params: params
+    assert_equal params,     PlaceReverseGeocoded.geocoder_options[:params]
+  end
+end
diff --git a/test/unit/mongoid_test.rb b/test/unit/mongoid_test.rb
new file mode 100644
index 0000000..8a9ca1a
--- /dev/null
+++ b/test/unit/mongoid_test.rb
@@ -0,0 +1,61 @@
+# encoding: utf-8
+require 'mongoid_test_helper'
+
+class MongoidTest < GeocoderTestCase
+  def test_geocoded_check
+    p = PlaceUsingMongoid.new(*geocoded_object_params(:msg))
+    p.location = [40.750354, -73.993371]
+    assert p.geocoded?
+  end
+
+  def test_geocoded_check_single_coord
+    p = PlaceUsingMongoid.new(*geocoded_object_params(:msg))
+    p.location = [40.750354, nil]
+    assert !p.geocoded?
+  end
+
+  def test_distance_to_returns_float
+    p = PlaceUsingMongoid.new(*geocoded_object_params(:msg))
+    p.location = [40.750354, -73.993371]
+    assert p.distance_to([30, -94]).is_a?(Float)
+  end
+
+  def test_model_configuration
+    p = PlaceUsingMongoid.new(*geocoded_object_params(:msg))
+    p.location = [0, 0]
+
+    PlaceUsingMongoid.geocoded_by :address, :coordinates => :location, :units => :km
+    assert_equal 111, p.distance_to([0,1]).round
+
+    PlaceUsingMongoid.geocoded_by :address, :coordinates => :location, :units => :mi
+    assert_equal 69, p.distance_to([0,1]).round
+  end
+
+  def test_index_is_skipped_if_skip_option_flag
+    if PlaceUsingMongoidWithoutIndex.respond_to?(:index_options)
+      result = PlaceUsingMongoidWithoutIndex.index_options.keys.flatten[0] == :coordinates
+    else
+      result = PlaceUsingMongoidWithoutIndex.index_specifications[0] == :coordinates
+    end
+    assert !result
+  end
+
+  def test_geocoded_with_custom_handling
+    p = PlaceUsingMongoidWithCustomResultsHandling.new(*geocoded_object_params(:msg))
+    p.location = [40.750354, -73.993371]
+    p.geocode
+    assert_match(/[0-9\.,\-]+/, p.coords_string)
+  end
+
+  def test_reverse_geocoded
+    p = PlaceUsingMongoidReverseGeocoded.new(*reverse_geocoded_object_params(:msg))
+    p.reverse_geocode
+    assert_match(/New York/, p.address)
+  end
+
+  def test_reverse_geocoded_with_custom_handling
+    p = PlaceUsingMongoidReverseGeocodedWithCustomResultsHandling.new(*reverse_geocoded_object_params(:msg))
+    p.reverse_geocode
+    assert_equal "US", p.country.upcase
+  end
+end
diff --git a/test/unit/near_test.rb b/test/unit/near_test.rb
new file mode 100644
index 0000000..7ba3506
--- /dev/null
+++ b/test/unit/near_test.rb
@@ -0,0 +1,108 @@
+# encoding: utf-8
+require 'test_helper'
+
+class NearTest < GeocoderTestCase
+
+  def test_near_scope_options_includes_bounding_box_condition
+    omit("Not applicable to unextended SQLite") if using_unextended_sqlite?
+
+    result = PlaceWithCustomResultsHandling.send(:near_scope_options, 1.0, 2.0, 5)
+    table_name = PlaceWithCustomResultsHandling.table_name
+    assert_match(/#{table_name}.latitude BETWEEN 0.9276\d* AND 1.0723\d* AND #{table_name}.longitude BETWEEN 1.9276\d* AND 2.0723\d* AND /, result[:conditions][0])
+  end
+
+  def test_near_scope_options_includes_radius_condition
+    omit("Not applicable to unextended SQLite") if using_unextended_sqlite?
+
+    result = Place.send(:near_scope_options, 1.0, 2.0, 5)
+    assert_match(/BETWEEN \? AND \?$/, result[:conditions][0])
+  end
+
+  def test_near_scope_options_includes_radius_column_max_radius
+    omit("Not applicable to unextended SQLite") if using_unextended_sqlite?
+
+    result = Place.send(:near_scope_options, 1.0, 2.0, :radius_column)
+    assert_match(/BETWEEN \? AND radius_column$/, result[:conditions][0])
+  end
+
+  def test_near_scope_options_includes_radius_default_min_radius
+    omit("Not applicable to unextended SQLite") if using_unextended_sqlite?
+
+    result = Place.send(:near_scope_options, 1.0, 2.0, 5)
+
+    assert_equal(0, result[:conditions][1])
+    assert_equal(5, result[:conditions][2])
+  end
+
+  def test_near_scope_options_includes_radius_custom_min_radius
+    omit("Not applicable to unextended SQLite") if using_unextended_sqlite?
+
+    result = Place.send(:near_scope_options, 1.0, 2.0, 5, :min_radius => 3)
+
+    assert_equal(3, result[:conditions][1])
+    assert_equal(5, result[:conditions][2])
+  end
+
+  def test_near_scope_options_includes_radius_bogus_min_radius
+    omit("Not applicable to unextended SQLite") if using_unextended_sqlite?
+    
+    result = Place.send(:near_scope_options, 1.0, 2.0, 5, :min_radius => 'bogus')
+
+    assert_equal(0, result[:conditions][1])
+    assert_equal(5, result[:conditions][2])
+  end
+
+  def test_near_scope_options_with_defaults
+    result = PlaceWithCustomResultsHandling.send(:near_scope_options, 1.0, 2.0, 5)
+
+    assert_match(/AS distance/, result[:select])
+    assert_match(/AS bearing/, result[:select])
+    assert_no_consecutive_comma(result[:select])
+  end
+
+  def test_near_scope_options_with_no_distance
+    result = PlaceWithCustomResultsHandling.send(:near_scope_options, 1.0, 2.0, 5, :select_distance => false)
+
+    assert_no_match(/AS distance/, result[:select])
+    assert_match(/AS bearing/, result[:select])
+    assert_no_match(/distance/, result[:condition])
+    assert_no_match(/distance/, result[:order])
+    assert_no_consecutive_comma(result[:select])
+  end
+
+  def test_near_scope_options_with_no_bearing
+    result = PlaceWithCustomResultsHandling.send(:near_scope_options, 1.0, 2.0, 5, :select_bearing => false)
+
+    assert_match(/AS distance/, result[:select])
+    assert_no_match(/AS bearing/, result[:select])
+    assert_no_consecutive_comma(result[:select])
+  end
+
+  def test_near_scope_options_with_custom_distance_column
+    result = PlaceWithCustomResultsHandling.send(:near_scope_options, 1.0, 2.0, 5, :distance_column => 'calculated_distance')
+
+    assert_no_match(/AS distance/, result[:select])
+    assert_match(/AS calculated_distance/, result[:select])
+    assert_no_match(/\bdistance\b/, result[:order])
+    assert_match(/calculated_distance/, result[:order])
+    assert_no_consecutive_comma(result[:select])
+  end
+
+  def test_near_scope_options_with_custom_bearing_column
+    result = PlaceWithCustomResultsHandling.send(:near_scope_options, 1.0, 2.0, 5, :bearing_column => 'calculated_bearing')
+
+    assert_no_match(/AS bearing/, result[:select])
+    assert_match(/AS calculated_bearing/, result[:select])
+    assert_no_consecutive_comma(result[:select])
+  end
+
+  private
+
+  def assert_no_consecutive_comma(string)
+    assert_no_match(/, *,/, string, "two consecutive commas")
+  end
+
+  def using_unextended_sqlite?
+    ENV['DB'] == 'sqlite' && ENV['USE_SQLITE_EXT'] != '1'
+  end
+end
diff --git a/test/unit/proxy_test.rb b/test/unit/proxy_test.rb
new file mode 100644
index 0000000..4c08e50
--- /dev/null
+++ b/test/unit/proxy_test.rb
@@ -0,0 +1,36 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ProxyTest < GeocoderTestCase
+
+  def test_uses_proxy_when_specified
+    Geocoder.configure(:http_proxy => 'localhost')
+    lookup = Geocoder::Lookup::Bing.new
+    assert lookup.send(:http_client).proxy_class?
+  end
+
+  def test_doesnt_use_proxy_when_not_specified
+    lookup = Geocoder::Lookup::Bing.new
+    assert !lookup.send(:http_client).proxy_class?
+  end
+
+  def test_exception_raised_on_bad_proxy_url
+    Geocoder.configure(:http_proxy => ' \\_O< Quack Quack')
+    assert_raise Geocoder::ConfigurationError do
+      Geocoder::Lookup::Bing.new.send(:http_client)
+    end
+  end
+
+  def test_accepts_proxy_with_http_protocol
+    Geocoder.configure(:http_proxy => 'http://localhost')
+    lookup = Geocoder::Lookup::Bing.new
+    assert lookup.send(:http_client).proxy_class?
+  end
+
+  def test_accepts_proxy_with_https_protocol
+    Geocoder.configure(:https_proxy => 'https://localhost')
+    Geocoder.configure(:use_https => true)
+    lookup = Geocoder::Lookup::Google.new
+    assert lookup.send(:http_client).proxy_class?
+  end
+end
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
new file mode 100644
index 0000000..5675461
--- /dev/null
+++ b/test/unit/query_test.rb
@@ -0,0 +1,93 @@
+# encoding: utf-8
+require 'test_helper'
+
+class QueryTest < GeocoderTestCase
+
+  def test_detect_ipv4
+    assert Geocoder::Query.new("232.65.123.94").ip_address?
+  end
+
+  def test_detect_ipv6
+    assert Geocoder::Query.new("3ffe:0b00:0000:0000:0001:0000:0000:000a").ip_address?
+  end
+
+  def test_detect_non_ip_address
+    assert !Geocoder::Query.new("232.65.123.94.43").ip_address?
+    assert !Geocoder::Query.new("::ffff:123.456.789").ip_address?
+  end
+
+  def test_blank_query_detection
+    assert Geocoder::Query.new(nil).blank?
+    assert Geocoder::Query.new("").blank?
+    assert Geocoder::Query.new("\t  ").blank?
+    assert !Geocoder::Query.new("a").blank?
+    assert !Geocoder::Query.new("Москва").blank? # no ASCII characters
+    assert !Geocoder::Query.new("\na").blank?
+
+    assert Geocoder::Query.new(nil, :params => {}).blank?
+    assert !Geocoder::Query.new(nil, :params => {:woeid => 1234567}).blank?
+  end
+
+  def test_blank_query_detection_for_coordinates
+    assert Geocoder::Query.new([nil,nil]).blank?
+    assert Geocoder::Query.new([87,nil]).blank?
+  end
+
+  def test_coordinates_detection
+    assert Geocoder::Query.new("51.178844,5").coordinates?
+    assert Geocoder::Query.new("51.178844, -1.826189").coordinates?
+    assert !Geocoder::Query.new("232.65.123").coordinates?
+    assert !Geocoder::Query.new("Test\n51.178844, -1.826189").coordinates?
+  end
+
+  def test_internal_ip_address
+    assert Geocoder::Query.new("127.0.0.1").internal_ip_address?
+    assert Geocoder::Query.new("172.19.0.1").internal_ip_address?
+    assert Geocoder::Query.new("10.100.100.1").internal_ip_address?
+    assert Geocoder::Query.new("192.168.0.1").internal_ip_address?
+    assert !Geocoder::Query.new("232.65.123.234").internal_ip_address?
+  end
+
+  def test_loopback_ip_address
+    assert Geocoder::Query.new("127.0.0.1").loopback_ip_address?
+    assert !Geocoder::Query.new("232.65.123.234").loopback_ip_address?
+  end
+
+  def test_private_ip_address
+    assert Geocoder::Query.new("172.19.0.1").private_ip_address?
+    assert Geocoder::Query.new("10.100.100.1").private_ip_address?
+    assert Geocoder::Query.new("192.168.0.1").private_ip_address?
+    assert !Geocoder::Query.new("127.0.0.1").private_ip_address?
+    assert !Geocoder::Query.new("232.65.123.234").private_ip_address?
+  end
+
+  def test_sanitized_text_with_array
+    q = Geocoder::Query.new([43.1313,11.3131])
+    assert_equal "43.1313,11.3131", q.sanitized_text
+  end
+
+  def test_custom_lookup
+    query = Geocoder::Query.new("address", :lookup => :nominatim)
+    assert_instance_of Geocoder::Lookup::Nominatim, query.lookup
+  end
+
+  def test_force_specify_ip_address
+    Geocoder.configure({:ip_lookup => :google})
+    query = Geocoder::Query.new("address", {:ip_address => true})
+    assert !query.ip_address?
+    assert_instance_of Geocoder::Lookup::Google, query.lookup
+  end
+
+  def test_force_specify_street_address
+    Geocoder.configure({:lookup => :google, :ip_lookup => :freegeoip})
+    query = Geocoder::Query.new("4.1.0.2", {street_address: true})
+    assert query.ip_address?
+    assert_instance_of Geocoder::Lookup::Google, query.lookup
+  end
+
+  def test_force_specify_ip_address_with_ip_lookup
+    query = Geocoder::Query.new("address", {:ip_address => true, :ip_lookup => :google})
+    assert !query.ip_address?
+    assert_instance_of Geocoder::Lookup::Google, query.lookup
+  end
+end
diff --git a/test/unit/request_test.rb b/test/unit/request_test.rb
new file mode 100644
index 0000000..a77f5bf
--- /dev/null
+++ b/test/unit/request_test.rb
@@ -0,0 +1,108 @@
+# encoding: utf-8
+require 'test_helper'
+
+class RequestTest < GeocoderTestCase
+  class MockRequest < Rack::Request
+    include Geocoder::Request
+    def initialize(headers={}, ip="")
+      super_env = headers
+      super_env.merge!({'REMOTE_ADDR' => ip}) unless ip == ""
+      super(super_env)
+    end
+  end
+
+  def setup
+    Geocoder.configure(ip_lookup: :freegeoip)
+  end
+
+  def test_http_x_real_ip
+    req = MockRequest.new({"HTTP_X_REAL_IP" => "74.200.247.59"})
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_http_x_client_ip
+    req = MockRequest.new({"HTTP_X_CLIENT_IP" => "74.200.247.59"})
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_http_x_cluster_client_ip
+    req = MockRequest.new({"HTTP_X_CLUSTER_CLIENT_IP" => "74.200.247.59"})
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_http_x_forwarded_for_without_proxy
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => "74.200.247.59"})
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_http_x_forwarded_for_with_proxy
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => "74.200.247.59, 74.200.247.60"})
+    assert req.geocoder_spoofable_ip == '74.200.247.59'
+    assert req.ip == '74.200.247.60'
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+    assert_equal "US", req.location.country_code
+  end
+  def test_safe_http_x_forwarded_for_with_proxy
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => "74.200.247.59, 74.200.247.60"})
+    assert req.geocoder_spoofable_ip == '74.200.247.59'
+    assert req.ip == '74.200.247.60'
+    assert req.safe_location.is_a?(Geocoder::Result::Freegeoip)
+    assert_equal "MX", req.safe_location.country_code
+  end
+  def test_with_request_ip
+    req = MockRequest.new({}, "74.200.247.59")
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_with_loopback_x_forwarded_for
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => "127.0.0.1"}, "74.200.247.59")
+    assert_equal "US", req.location.country_code
+  end
+  def test_with_private_x_forwarded_for
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => "172.19.0.1"}, "74.200.247.59")
+    assert_equal "US", req.location.country_code
+  end
+  def test_http_x_forwarded_for_with_misconfigured_proxies
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => ","}, "74.200.247.59")
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_non_ip_in_proxy_header
+    req = MockRequest.new({"HTTP_X_FORWARDED_FOR" => "Albequerque NM"})
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_safe_location_after_location
+    req = MockRequest.new({"HTTP_X_REAL_IP" => "74.200.247.59"}, "127.0.0.1")
+    assert_equal 'US', req.location.country_code
+    assert_equal 'RD', req.safe_location.country_code
+  end
+  def test_location_after_safe_location
+    req = MockRequest.new({'HTTP_X_REAL_IP' => '74.200.247.59'}, '127.0.0.1')
+    assert_equal 'RD', req.safe_location.country_code
+    assert_equal 'US', req.location.country_code
+  end
+  def test_geocoder_remove_port_from_addresses_with_port
+    expected_ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3']
+    ips = ['127.0.0.1:3000', '127.0.0.2:8080', '127.0.0.3:9292']
+    req = MockRequest.new()
+    assert_equal expected_ips, req.send(:geocoder_remove_port_from_addresses, ips)
+  end
+  def test_geocoder_remove_port_from_ipv6_addresses_with_port
+    expected_ips = ['2600:1008:b16e:26da:ecb3:22f7:6be4:2137', '2600:1901:0:2df5::', '2001:db8:1f70::999:de8:7648:6e8', '10.128.0.2']
+    ips = ['2600:1008:b16e:26da:ecb3:22f7:6be4:2137', '2600:1901:0:2df5::', '[2001:db8:1f70::999:de8:7648:6e8]:100', '10.128.0.2']
+    req = MockRequest.new()
+    assert_equal expected_ips, req.send(:geocoder_remove_port_from_addresses, ips)
+  end
+  def test_geocoder_remove_port_from_addresses_without_port
+    expected_ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3']
+    ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3']
+    req = MockRequest.new()
+    assert_equal expected_ips, req.send(:geocoder_remove_port_from_addresses, ips)
+  end
+  def test_geocoder_reject_non_ipv4_addresses_with_good_ips
+    expected_ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3']
+    ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3']
+    req = MockRequest.new()
+    assert_equal expected_ips, req.send(:geocoder_reject_non_ipv4_addresses, ips)
+  end
+  def test_geocoder_reject_non_ipv4_addresses_with_bad_ips
+    expected_ips = ['127.0.0.1']
+    ips = ['127.0.0', '127.0.0.1', '127.0.0.2.0']
+    req = MockRequest.new()
+    assert_equal expected_ips, req.send(:geocoder_reject_non_ipv4_addresses, ips)
+  end
+end
diff --git a/test/unit/result_test.rb b/test/unit/result_test.rb
new file mode 100644
index 0000000..b948b8a
--- /dev/null
+++ b/test/unit/result_test.rb
@@ -0,0 +1,55 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ResultTest < GeocoderTestCase
+
+  def test_forward_geocoding_result_has_required_attributes
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      next if [
+        :ip2location, # has pay-per-attribute pricing model
+        :twogis, # cant find 'Madison Square Garden'
+      ].include?(l)
+
+      Geocoder.configure(:lookup => l)
+      set_api_key!(l)
+      result = Geocoder.search("Madison Square Garden").first
+      assert_result_has_required_attributes(result)
+    end
+  end
+
+  def test_reverse_geocoding_result_has_required_attributes
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      next if [
+        :ip2location, # has pay-per-attribute pricing model
+        :nationaal_georegister_nl, # no reverse geocoding
+        :melissa_street, # reverse geocoding not implemented
+        :twogis, # cant find 'Madison Square Garden'
+      ].include?(l)
+
+      Geocoder.configure(:lookup => l)
+      set_api_key!(l)
+      result = Geocoder.search([45.423733, -75.676333]).first
+      assert_result_has_required_attributes(result)
+    end
+  end
+
+  private # ------------------------------------------------------------------
+
+  def assert_result_has_required_attributes(result)
+    m = "Lookup #{Geocoder.config.lookup} does not support %s attribute."
+    assert result.coordinates.is_a?(Array),    m % "coordinates"
+    assert result.latitude.is_a?(Float),       m % "latitude"
+    assert result.latitude != 0.0,             m % "latitude"
+    assert result.longitude.is_a?(Float),      m % "longitude"
+    assert result.longitude != 0.0,            m % "longitude"
+    assert result.city.is_a?(String),          m % "city"
+    assert result.state.is_a?(String),         m % "state"
+    assert result.state_code.is_a?(String),    m % "state_code"
+    assert result.province.is_a?(String),      m % "province"
+    assert result.province_code.is_a?(String), m % "province_code"
+    assert result.postal_code.is_a?(String),   m % "postal_code"
+    assert result.country.is_a?(String),       m % "country"
+    assert result.country_code.is_a?(String),  m % "country_code"
+    assert_not_nil result.address,             m % "address"
+  end
+end
diff --git a/test/unit/test_mode_test.rb b/test/unit/test_mode_test.rb
new file mode 100644
index 0000000..e1d7ace
--- /dev/null
+++ b/test/unit/test_mode_test.rb
@@ -0,0 +1,91 @@
+# encoding: utf-8
+require 'test_helper'
+
+class TestModeTest < GeocoderTestCase
+
+  def setup
+    @_original_lookup = Geocoder.config.lookup
+    Geocoder.configure(:lookup => :test)
+  end
+
+  def teardown
+    Geocoder::Lookup::Test.reset
+    Geocoder.configure(:lookup => @_original_lookup)
+  end
+
+  def test_search_with_known_stub
+    Geocoder::Lookup::Test.add_stub("New York, NY", [mock_attributes])
+
+    results = Geocoder.search("New York, NY")
+    result = results.first
+
+    assert_equal 1, results.size
+    mock_attributes.each_key do |attr|
+      assert_equal mock_attributes[attr], result.send(attr)
+    end
+  end
+
+  def test_search_with_unknown_stub_without_default
+    assert_raise ArgumentError do
+      Geocoder.search("New York, NY")
+    end
+  end
+
+  def test_search_with_unknown_stub_with_default
+    Geocoder::Lookup::Test.set_default_stub([mock_attributes])
+
+    results = Geocoder.search("Atlantis, OC")
+    result = results.first
+
+    assert_equal 1, results.size
+    mock_attributes.keys.each do |attr|
+      assert_equal mock_attributes[attr], result.send(attr)
+    end
+  end
+
+  def test_search_with_custom_attributes
+    custom_attributes = mock_attributes.merge(:custom => 'NY, NY')
+    Geocoder::Lookup::Test.add_stub("New York, NY", [custom_attributes])
+
+    result = Geocoder.search("New York, NY").first
+
+    assert_equal 'NY, NY', result.custom
+  end
+
+  def test_search_with_invalid_address_stub
+    Geocoder::Lookup::Test.add_stub("invalid address/no result", [])
+
+    result = Geocoder.search("invalid address/no result")
+
+    assert_equal [], result
+  end
+
+  def test_unsetting_stub
+    Geocoder::Lookup::Test.add_stub("New York, NY", [mock_attributes])
+
+    assert_nothing_raised ArgumentError do
+      Geocoder.search("New York, NY")
+    end
+
+    Geocoder::Lookup::Test.delete_stub("New York, NY")
+
+    assert_raise ArgumentError do
+      Geocoder.search("New York, NY")
+    end
+  end
+
+  private
+  def mock_attributes
+    coordinates = [40.7143528, -74.0059731]
+    @mock_attributes ||= {
+      'coordinates'  => coordinates,
+      'latitude'     => coordinates[0],
+      'longitude'    => coordinates[1],
+      'address'      => 'New York, NY, USA',
+      'state'        => 'New York',
+      'state_code'   => 'NY',
+      'country'      => 'United States',
+      'country_code' => 'US',
+    }
+  end
+end
diff --git a/test/unit/util_test.rb b/test/unit/util_test.rb
new file mode 100644
index 0000000..f3b9411
--- /dev/null
+++ b/test/unit/util_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class UtilTest < GeocoderTestCase
+  def test_recursive_hash_merge
+    h1 = { 'a' => 100, 'b' => 200, 'c' => { 'c1' => 12, 'c2' => 14 } }
+    h2 = { 'b' => 254, 'c' => { 'c1' => 16, 'c3' => 94 } }
+    Geocoder::Util.recursive_hash_merge(h1, h2)
+    assert h1 == { 'a' => 100, 'b' => 254, 'c' => { 'c1' => 16, 'c2' => 14, 'c3' => 94 } }
+  end
+end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/easting_northing.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/cache_stores/base.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/cache_stores/generic.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/cache_stores/redis.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/abstract_api.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/amazon_location_service.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/geoapify.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/ipbase.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/ipgeolocation.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/ipqualityscore.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/ipregistry.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/melissa_street.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/nationaal_georegister_nl.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/osmnames.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/photon.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/twogis.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/uk_ordnance_survey_names.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/abstract_api.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/amazon_location_service.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/geoapify.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/ipbase.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/ipgeolocation.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/ipqualityscore.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/ipregistry.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/melissa_street.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/nationaal_georegister_nl.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/osmnames.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/photon.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/twogis.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/uk_ordnance_survey_names.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/util.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/geocoder-1.8.1.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/lookups/geocoder_us.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/geocoder/results/geocoder_us.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/hash_recursive_merge.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/geocoder-1.5.1.gemspec

No differences were encountered in the control files

More details

Full run details