diff --git a/.gitignore b/.gitignore
index a9305609fd2a51a9899e8a2ef024a70646f496d8..c94bfb4e2fb82925be8231893229c1e294f40b68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ pkg/*
 rdoc/*
 *.gem
 .bundle
+Gemfile.lock
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..460484cd92e691c512288eced943091a07cf11df
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+rvm:
+  - 1.8.7
+  - 1.9.2
+  - 1.9.3
+  - jruby-19mode
+gemfile:
+  - Gemfile
+  - gemfiles/Gemfile.mongoid-2.4.x
+env: SSL_CERT_DIR=/etc/ssl/certs
+matrix:
+  exclude:
+    - rvm: 1.8.7
+      gemfile: Gemfile
+      env: SSL_CERT_DIR=/etc/ssl/certs
+    - rvm: 1.9.2
+      gemfile: Gemfile
+      env: SSL_CERT_DIR=/etc/ssl/certs
+    - rvm: 1.9.3
+      gemfile: gemfiles/Gemfile.mongoid-2.4.x
+      env: SSL_CERT_DIR=/etc/ssl/certs
+    - rvm: jruby-19mode
+      gemfile: gemfiles/Gemfile.mongoid-2.4.x
+      env: SSL_CERT_DIR=/etc/ssl/certs 
diff --git a/CHANGELOG.rdoc b/CHANGELOG.md
similarity index 59%
rename from CHANGELOG.rdoc
rename to CHANGELOG.md
index 80b266ed33d129406bc1a24d91396e0261b3d663..6d99776237bd87d18dd7f52c9cf60f8ad9d36747 100644
--- a/CHANGELOG.rdoc
+++ b/CHANGELOG.md
@@ -1,8 +1,75 @@
-= Changelog
+Changelog
+=========
 
 Per-release changes to Geocoder.
 
-== 1.1.1 (2012 Feb 16)
+1.1.6 (2012 Dec 24)
+-------------------
+
+* Major changes to configuration syntax which allow for API-specific config options. Old config syntax is now DEPRECATED.
+* Add support for MaxMind API (thanks github.com/gonzoyumo).
+* Add optional Geocoder::InvalidApiKey exception for bad API credentials (Yahoo, Yandex, Bing, and Maxmind). Warn when bad key and exception not set in Geocoder.configure(:always_raise => [...]).
+* Add support for X-Real-IP and X-Forwarded-For headers (thanks github.com/konsti).
+* Add support for custom Nominatim host config: Geocoder.configure(:nominatim => {:host => "..."}).
+* Raise exception when required API key is missing or incorrect format.
+* Add support for Google's :region and :components parameters (thanks to github.com/tomlion).
+* Fix: string escaping bug in OAuth lib (thanks github.com/m0thman).
+* Fix: configured units were not always respected in SQL queries.
+* Fix: in #nearbys, don't try to exclude self if not yet persisted.
+* Fix: bug with cache stores that provided #delete but not #del.
+* Change #nearbys so that it returns nil instead of [] when object is not geocoded.
+
+1.1.5 (2012 Nov 9)
+------------------
+
+* Replace support for old Yahoo Placefinder with Yahoo BOSS (thanks github.com/pwoltman).
+* Add support for actual Mapquest API (was previously just a proxy for Nominatim), including the paid service (thanks github.com/jedschneider).
+* Add support for :select => :id_only option to near scope.
+* Treat a given query as blank (don't do a lookup) if coordinates are given but latitude or longitude is nil.
+* Speed up 'near' queries by adding bounding box condition (thanks github.com/mlandauer).
+* Fix: don't redefine Object#hash in Yahoo result object (thanks github.com/m0thman).
+
+1.1.4 (2012 Oct 2)
+------------------
+
+* Deprecate Geocoder::Result::Nominatim#class and #type methods. Use #place_class and #place_type instead.
+* Add support for setting arbitrary parameters in geocoding request URL.
+* Add support for Google's :bounds parameter (thanks to github.com/rosscooperman and github.com/peterjm for submitting suggestions).
+* Add support for :select => :geo_only option to near scope (thanks github.com/gugl).
+* Add ability to omit ORDER BY clause from .near scope (pass option :order => false).
+* Fix: error on Yahoo lookup due to API change (thanks github.com/kynesun).
+* Fix: problem with Mongoid field aliases not being respected.
+* Fix: :exclude option to .near scope when primary key != :id (thanks github.com/smisml).
+* Much code refactoring (added Geocoder::Query class and Geocoder::Sql module).
+
+1.1.3 (2012 Aug 26)
+-------------------
+
+* Add support for Mapquest geocoding service (thanks github.com/razorinc).
+* Add :test lookup for easy testing of apps using Geocoder (thanks github.com/mguterl).
+* Add #precision method to Yandex results (thanks github.com/gemaker).
+* Add support for raising :all exceptions (thanks github.com/andyvb).
+* Add exceptions for certain Google geocoder responses (thanks github.com/andyvb).
+* Add Travis-CI integration (thanks github.com/petergoldstein).
+* Fix: unit config was not working with SQLite (thanks github.com/balvig).
+* Fix: get tests to pass under Jruby (thanks github.com/petergoldstein).
+* Fix: bug in distance_from_sql method (error occurred when coordinates not found).
+* Fix: incompatibility with Mongoid 3.0.x (thanks github.com/petergoldstein).
+
+1.1.2 (2012 May 24)
+-------------------
+
+* Add ability to specify default units and distance calculation method (thanks github.com/abravalheri).
+* Add new (optional) configuration syntax (thanks github.com/abravalheri).
+* Add support for cache stores that provide :get and :set methods.
+* Add support for custom HTTP request headers (thanks github.com/robotmay).
+* Add Result#cache_hit attribute (thanks github.com/s01ipsist).
+* Fix: rake geocode:all wasn't properly loading namespaced classes.
+* Fix: properly recognize IP addresses with ::ffff: prefix (thanks github.com/brian-ewell).
+* Fix: avoid exception during calculations when coordinates not known (thanks github.com/flori).
+
+1.1.1 (2012 Feb 16)
+-------------------
 
 * Add distance_from_sql class method to geocoded class (thanks github.com/dwilkie).
 * Add OverQueryLimitError and raise when relevant for Google lookup.
@@ -10,7 +77,8 @@ Per-release changes to Geocoder.
 * Fix: within_bounding_box now uses correct lat/lon DB columns (thanks github.com/kongo).
 * Fix: error accessing city in some cases with Yandex result (thanks github.com/kor6n and sld).
 
-== 1.1.0 (2011 Dec 3)
+1.1.0 (2011 Dec 3)
+------------------
 
 * A block passed to geocoded_by is now always executed, even if the geocoding service returns no results. This means you need to make sure you have results before trying to assign data to your object.
 * Fix issues with joins and row counts (issues #49, 86, and 108) by not using GROUP BY clause with ActiveRecord scopes.
@@ -22,17 +90,20 @@ Per-release changes to Geocoder.
 * Add support for API key to Geocoder.ca geocoding service (thanks github.com/ryanLonac).
 * Add support for state to Yandex results (thanks github.com/tipugin).
 
-== 1.0.5 (2011 Oct 26)
+1.0.5 (2011 Oct 26)
+-------------------
 
 * Fix error with `rake assets:precompile` (thanks github.com/Sush).
 * Fix HTTPS support (thanks github.com/rsanheim).
 * Improve cache interface.
 
-== 1.0.4 (2011 Sep 18)
+1.0.4 (2011 Sep 18)
+-------------------
 
 * Remove klass method from rake task, which could conflict with app methods (thanks github.com/mguterl).
 
-== 1.0.3 (2011 Sep 17)
+1.0.3 (2011 Sep 17)
+-------------------
 
 * Add support for Google Premier geocoding service (thanks github.com/steveh).
 * Update Google API URL (thanks github.com/soorajb).
@@ -40,19 +111,22 @@ Per-release changes to Geocoder.
 * Fix: rake assets:precompile (Rails 3.1) not working in some situations.
 * Fix: stop double-adjusting units when using kilometers (thanks github.com/hairyheron).
 
-== 1.0.2 (2011 June 25)
+1.0.2 (2011 June 25)
+--------------------
 
 * Add support for MongoMapper (thanks github.com/spagalloco).
 * Fix: user-specified coordinates field wasn't working with Mongoid (thanks github.com/thisduck).
 * Fix: invalid location given to near scope was returning all results (Active Record) or error (Mongoid) (thanks github.com/ogennadi).
 
-== 1.0.1 (2011 May 17)
+1.0.1 (2011 May 17)
+-------------------
 
 * Add option to not rescue from certain exceptions (thanks github.com/ahmedrb).
 * Fix STI child/parent geocoding bug (thanks github.com/ogennadi).
 * Other bugfixes.
 
-== 1.0.0 (2011 May 9)
+1.0.0 (2011 May 9)
+------------------
 
 * Add command line interface.
 * Add support for local proxy (thanks github.com/Olivier).
@@ -61,11 +135,13 @@ Per-release changes to Geocoder.
 * Fix single table inheritance bug (reported by github.com/enrico).
 * Fix bug when Google result supplies no city (thanks github.com/jkeen).
 
-== 0.9.13 (2011 Apr 11)
+0.9.13 (2011 Apr 11)
+--------------------
 
 * Fix "can't find special index: 2d" error when using Mongoid with Ruby 1.8.
 
-== 0.9.12 (2011 Apr 6)
+0.9.12 (2011 Apr 6)
+-------------------
 
 * Add support for Mongoid.
 * Add bearing_to/from methods to geocoded objects.
@@ -76,7 +152,8 @@ Per-release changes to Geocoder.
 * DEPRECATION: Geocoder.near should not take <tt>:limit</tt> or <tt>:offset</tt> options.
 * DEPRECATION: Change argument format of all methods that take lat/lon as separate arguments. Now you must pass the coordinates as an array [lat,lon], but you may alternatively pass a address string (will look up coordinates) or a geocoded object (or any object that implements a to_coordinates method which returns a [lat,lon] array).
 
-== 0.9.11 (2011 Mar 25)
+0.9.11 (2011 Mar 25)
+--------------------
 
 * Add support for result caching.
 * Add support for Geocoder.ca geocoding service.
@@ -86,12 +163,14 @@ Per-release changes to Geocoder.
 * DEPRECATION: <tt>Geocoder.search</tt> now returns an array instead of a single result.
 * DEPRECATION: <tt>obj.nearbys</tt> second argument is now an options hash (instead of units). Please change <tt>obj.nearbys(20, :km)</tt> to: <tt>obj.nearbys(20, :units => :km)</tt>.
 
-== 0.9.10 (2011 Mar 9)
+0.9.10 (2011 Mar 9)
+-------------------
 
 * Fix broken scopes (github.com/mikepinde).
 * Fix broken Ruby 1.9 and JRuby compatibility (don't require json gem).
 
-== 0.9.9 (2011 Mar 9)
+0.9.9 (2011 Mar 9)
+------------------
 
 * Add support for IP address geocoding via FreeGeoIp.net.
 * Add support for Yahoo PlaceFinder geocoding API.
@@ -106,7 +185,8 @@ Per-release changes to Geocoder.
 * DEPRECATION: <tt>fetch_address!</tt> has been superceded by +reverse_geocode+ (then save your object manually).
 * Fix: don't die when trying to get coordinates with a nil address (github.com/zmack).
 
-== 0.9.8 (2011 Feb 8)
+0.9.8 (2011 Feb 8)
+------------------
 
 * Include <tt>geocode:all</tt> Rake task in gem (was missing!).
 * Add <tt>Geocoder.search</tt> for access to Google's full response.
@@ -114,87 +194,105 @@ Per-release changes to Geocoder.
 * Emit warnings on Google connection problems and errors.
 * Refactor: insert Geocoder into ActiveRecord via Railtie.
 
-== 0.9.7 (2011 Feb 1)
+0.9.7 (2011 Feb 1)
+------------------
 
 * Add reverse geocoding (+reverse_geocoded_by+).
 * Prevent exception (uninitialized constant Geocoder::Net) when net/http not already required (github.com/sleepycat).
 * Refactor: split monolithic Geocoder module into several smaller ones.
 
-== 0.9.6 (2011 Jan 19)
+0.9.6 (2011 Jan 19)
+-------------------
 
 * Fix incompatibility with will_paginate gem.
 * Include table names in GROUP BY clause of nearby scope to avoid ambiguity in joins (github.com/matchu).
 
-== 0.9.5 (2010 Oct 15)
+0.9.5 (2010 Oct 15)
+-------------------
 
 * Fix broken PostgreSQL compatibility (now 100% compatible).
 * Switch from Google's XML to JSON geocoding API.
 * Separate Rails 2 and Rails 3-compatible branches.
 * Don't allow :conditions hash in 'options' argument to 'nearbys' method (was deprecated in 0.9.3).
 
-== 0.9.4 (2010 Aug 2)
+0.9.4 (2010 Aug 2)
+------------------
 
 * Google Maps API key no longer required (uses geocoder v3).
 
-== 0.9.3 (2010 Aug 2)
+0.9.3 (2010 Aug 2)
+------------------
 
 * Fix incompatibility with Rails 3 RC 1.
 * Deprecate 'options' argument to 'nearbys' method.
 * Allow inclusion of 'nearbys' in Arel method chains.
 
-== 0.9.2 (2010 Jun 3)
+0.9.2 (2010 Jun 3)
+------------------
 
 * Fix LIMIT clause bug in PostgreSQL (reported by github.com/kenzie).
 
-== 0.9.1 (2010 May 4)
+0.9.1 (2010 May 4)
+------------------
 
 * Use scope instead of named_scope in Rails 3.
 
-== 0.9.0 (2010 Apr 2)
+0.9.0 (2010 Apr 2)
+------------------
 
 * Fix bug in PostgreSQL support (caused "PGError: ERROR:  column "distance" does not exist"), reported by github.com/developish.
 
-== 0.8.9 (2010 Feb 11)
+0.8.9 (2010 Feb 11)
+-------------------
 
 * Add Rails 3 compatibility.
 * Avoid querying Google when query would be an empty string.
 
-== 0.8.8 (2009 Dec 7)
+0.8.8 (2009 Dec 7)
+------------------
 
 * Automatically select a less accurate but compatible distance algorithm when SQLite database detected (fixes SQLite incompatibility).
 
-== 0.8.7 (2009 Nov 4)
+0.8.7 (2009 Nov 4)
+------------------
 
 * Added Geocoder.geographic_center method.
 * Replaced _get_coordinates class method with read_coordinates instance method.
 
-== 0.8.6 (2009 Oct 27)
+0.8.6 (2009 Oct 27)
+-------------------
 
 * The fetch_coordinates method now assigns coordinates to attributes (behaves like fetch_coordinates! used to) and fetch_coordinates! both assigns and saves the attributes.
 * Added geocode:all rake task.
 
-== 0.8.5 (2009 Oct 26)
+0.8.5 (2009 Oct 26)
+-------------------
 
 * Avoid calling deprecated method from within Geocoder itself.
 
-== 0.8.4 (2009 Oct 23)
+0.8.4 (2009 Oct 23)
+-------------------
 
 * Deprecate <tt>find_near</tt> class method in favor of +near+ named scope.
 
-== 0.8.3 (2009 Oct 23)
+0.8.3 (2009 Oct 23)
+-------------------
 
 * Update Google URL query string parameter to reflect recent changes in Google's API.
 
-== 0.8.2 (2009 Oct 12)
+0.8.2 (2009 Oct 12)
+-------------------
 
 * Allow a model's geocoder search string method to be something other than an ActiveRecord attribute.
 * Clean up documentation.
 
-== 0.8.1 (2009 Oct 8)
+0.8.1 (2009 Oct 8)
+------------------
 
 * Extract XML-fetching code from <tt>Geocoder.search</tt> and place in Geocoder._fetch_xml (for ease of mocking).
 * Add tests for coordinate-fetching instance methods.
 
-== 0.8.0 (2009 Oct 1)
+0.8.0 (2009 Oct 1)
+------------------
 
 First release.
diff --git a/Gemfile b/Gemfile
index c80ee3697054566d1a4247d80be78ec3ddfde295..85c71ce03310e9930678f9c4b5bd51b719aa3a9e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,15 @@
 source "http://rubygems.org"
 
+group :development, :test do
+  gem 'rake'
+  gem 'mongoid', '3.0.13'
+  gem 'bson_ext', :platforms => :ruby
+
+  gem 'rails'
+
+  platforms :jruby do
+  	gem 'jruby-openssl'
+  end
+end
+
 gemspec
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..82b218e3ec3491da08a970875825c05b967e42c4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,645 @@
+Geocoder
+========
+
+Geocoder is a complete geocoding solution for Ruby. With Rails it adds geocoding (by street or IP address), reverse geocoding (find street address based on given coordinates), and distance queries. It's as simple as calling `geocode` on your objects, and then using a scope like `Venue.near("Billings, MT")`.
+
+
+Compatibility
+-------------
+
+* Supports multiple Ruby versions: Ruby 1.8.7, 1.9.2, 1.9.3, and JRuby.
+* Supports multiple databases: MySQL, PostgreSQL, SQLite, and MongoDB (1.7.0 and higher).
+* Supports Rails 3.x. 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.
+
+
+Installation
+------------
+
+Install Geocoder like any other Ruby gem:
+
+    gem install geocoder
+
+Or, if you're using Rails/Bundler, add this to your Gemfile:
+
+    gem "geocoder"
+
+and run at the command prompt:
+
+    bundle install
+
+
+Object Geocoding
+----------------
+
+### ActiveRecord
+
+Your model must have two attributes (database columns) for storing latitude and longitude coordinates. By default they should be called `latitude` and `longitude` but this can be changed (see "Model Configuration" below):
+
+    rails generate migration AddLatitudeAndLongitudeToModel latitude:float longitude:float
+    rake db:migrate
+
+For reverse geocoding your model must provide a method that returns an address. 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`).
+
+Next, your model must tell Geocoder which method returns your object's geocodable address:
+
+    geocoded_by :full_street_address   # can also be an IP address
+    after_validation :geocode          # auto-fetch coordinates
+
+For reverse geocoding, tell Geocoder which attributes store latitude and longitude:
+
+    reverse_geocoded_by :latitude, :longitude
+    after_validation :reverse_geocode  # auto-fetch address
+
+### Mongoid
+
+First, your model must have an array field for storing coordinates:
+
+    field :coordinates, :type => Array
+
+You may also want an address field, like this:
+
+    field :address
+
+but if you store address components (city, state, country, etc) in separate fields you can instead define a method called `address` that combines them into a single string which will be used to query the geocoding service.
+
+Once your fields are defined, include the `Geocoder::Model::Mongoid` module and then call `geocoded_by`:
+
+    include Geocoder::Model::Mongoid
+    geocoded_by :address               # can also be an IP address
+    after_validation :geocode          # auto-fetch coordinates
+
+Reverse geocoding is similar:
+
+    include Geocoder::Model::Mongoid
+    reverse_geocoded_by :coordinates
+    after_validation :reverse_geocode  # auto-fetch address
+
+Once you've set up your model you'll need to create the necessary spatial indices in your database:
+
+    rake db:mongoid:create_indexes
+
+Be sure to read _Latitude/Longitude Order_ in the _Notes on MongoDB_ section below on how to properly retrieve latitude/longitude coordinates from your objects.
+
+### MongoMapper
+
+MongoMapper is very similar to Mongoid, just be sure to include `Geocoder::Model::MongoMapper`.
+
+### Mongo Indices
+
+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
+
+### Bulk 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
+
+Geocoder will print warnings if you exceed the rate limit for your geocoding service.
+
+
+Request Geocoding by IP Address
+-------------------------------
+
+Geocoder adds a `location` method 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
+
+See _Advanced Geocoding_ below for more information about `Geocoder::Result` objects.
+
+
+Location-Aware Database Queries
+-------------------------------
+
+To find objects by location, use the following scopes:
+
+    Venue.near('Omaha, NE, US', 20)    # venues within 20 miles of Omaha
+    Venue.near([40.71, 100.23], 20)    # venues within 20 miles 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
+
+Some utility methods are also available:
+
+    # look up coordinates of some location (like searching Google Maps)
+    Geocoder.coordinates("25 Main St, Cooperstown, NY")
+     => [42.700149, -74.922767]
+
+    # distance (in miles) between Eiffel Tower and Empire State Building
+    Geocoder::Calculations.distance_between([47.858205,2.294359], [40.748433,-73.985655])
+     => 3619.77359999382
+
+    # 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]
+
+Please see the code for more methods and detailed information about arguments (eg, working with kilometers).
+
+
+Distance and Bearing
+--------------------
+
+When you run a location-aware query the returned objects have two attributes added to them (only w/ ActiveRecord):
+
+* `obj.distance` - number of miles from the search point to this object
+* `obj.bearing` - direction from the search point to this object
+
+Results are automatically sorted by distance from the search point, closest to farthest. Bearing is given as a number of clockwise degrees from due north, for example:
+
+* `0` - due north
+* `180` - due south
+* `90` - due east
+* `270` - due west
+* `230.1` - southwest
+* `359.9` - almost due north
+
+You can convert these numbers to compass point names by using the utility method provided:
+
+    Geocoder::Calculations.compass_point(355) # => "N"
+    Geocoder::Calculations.compass_point(45)  # => "NE"
+    Geocoder::Calculations.compass_point(208) # => "SW"
+
+_Note: when using SQLite `distance` and `bearing` values are provided for interface consistency only. They are not very accurate._
+
+To calculate accurate distance and bearing with SQLite or MongoDB:
+
+    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` or `:km`).
+
+
+Model Configuration
+-------------------
+
+You are not stuck with using 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
+
+The `address` method can return any string you'd use to search Google Maps. For example, any of the following are acceptable:
+
+* "714 Green St, Big Town, MO"
+* "Eiffel Tower, Paris, FR"
+* "Paris, TX, US"
+
+If your model has `street`, `city`, `state`, and `country` attributes you might do something like this:
+
+    geocoded_by :address
+
+    def address
+      [street, city, state, country].compact.join(', ')
+    end
+
+For reverse geocoding you can also specify an alternate name attribute where the address will be stored, for example:
+
+    reverse_geocoded_by :latitude, :longitude, :address => :location  # ActiveRecord
+    reverse_geocoded_by :coordinates, :address => :loc                # MongoDB
+
+
+Advanced Querying
+-----------------
+
+When querying for objects (if you're using ActiveRecord) you can also look within a square rather than a radius (circle) by using the `within_bounding_box` scope:
+
+    distance = 20
+    center_point = [40.71, 100.23]
+    box = Geocoder::Calculations.bounding_box(center_point, distance)
+    Venue.within_bounding_box(box)
+
+This can also dramatically improve query performance, especially when used in conjunction with indexes on the latitude/longitude columns. Note, however, that returned results do not include `distance` and `bearing` attributes. If you want to improve performance AND have access to distance and bearing info, use both scopes:
+
+    Venue.near(center_point, distance).within_bounding_box(box)
+
+
+Advanced Geocoding
+------------------
+
+So far we have looked at shortcuts for assigning geocoding results to object attributes. However, if you need to do something fancy you can skip the auto-assignment by providing a block (takes the object to be geocoded and an array of `Geocoder::Result` objects) in which you handle the parsed geocoding result 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
+
+Every `Geocoder::Result` object, `result`, provides the following data:
+
+* `result.latitude` - float
+* `result.longitude` - float
+* `result.coordinates` - array of the above two
+* `result.address` - string
+* `result.city` - string
+* `result.state` - string
+* `result.state_code` - string
+* `result.postal_code` - string
+* `result.country` - string
+* `result.country_code` - string
+
+If you're familiar with the results returned by the geocoding service you're using you can access even more data, but you'll need to be familiar with the particular `Geocoder::Result` object you're using and the structure of your geocoding service's responses. (See below for links to geocoding service documentation.)
+
+
+Geocoding Services
+------------------
+
+By default Geocoder uses Google's geocoding API to fetch coordinates and street addresses (FreeGeoIP is the default for IP address info). However there are several other APIs supported, as well as a variety of settings. Please see the listing and comparison below for details on specific geocoding services (not all settings are supported by all services). Some common configuration options are:
+
+    # config/initializers/geocoder.rb
+    Geocoder.configure(
+
+      # geocoding service (see below for supported options):
+      :lookup => :yandex,
+
+      # to use an API key:
+      :api_key => "...",
+
+      # geocoding service request timeout, in seconds (default 3):
+      :timeout => 5,
+
+      # set default units to kilometers:
+      :units => :km,
+
+      # caching (see below for details):
+      :cache => Redis.new,
+      :cache_prefix => "..."
+
+    )
+
+Please see lib/geocoder/configuration.rb for a complete list of configuration options. Additionally, some lookups have their own configuration options, some of which are directly supported by Geocoder. For example, to specify a value for Google's `bounds` parameter:
+
+    # with Google:
+    Geocoder.search("Paris", :bounds => [[32.1,-95.9], [33.9,-94.3]])
+
+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, by which you can pass arbitrary parameters to any geocoding service. For example, to use Nominatim's `countrycodes` parameter:
+
+    # with Nominatim:
+    Geocoder.search("Paris", :params => {:countrycodes => "gb,de,fr,es,us"})
+
+
+### Listing and Comparison
+
+The following is a comparison of the supported geocoding APIs. The "Limitations" listed for each are a very brief and incomplete summary of some special limitations beyond basic data source attribution. Please read the official Terms of Service for a service before using it.
+
+#### Google (`:google`, `:google_premier`)
+
+* **API key**: required for Premier (do NOT use a key for the free version)
+* **Key signup**: http://code.google.com/apis/maps/signup.html
+* **Quota**: 2,500 requests/day, 100,000 with Google Maps API Premier
+* **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 options**: `:bounds` - pass SW and NE coordinates as an array of two arrays to bias results towards a viewport
+* **Documentation**: http://code.google.com/apis/maps/documentation/geocoding/#JSON
+* **Terms of Service**: http://code.google.com/apis/maps/terms.html#section_10_12
+* **Limitations**: "You must not use or display the Content without a corresponding Google map, unless you are explicitly permitted to do so in the Maps APIs Documentation, or through written permission from Google." "You must not pre-fetch, cache, or store any Content, except that you may store: (i) limited amounts of Content for the purpose of improving the performance of your Maps API Implementation..."
+* **Notes**: To use Google Premier set `Geocoder.configure(:lookup => :google_premier, :api_key => [key, client, channel])`.
+
+#### Yahoo BOSS (`:yahoo`)
+
+Yahoo BOSS is **not a free service**. As of November 17, 2012 Yahoo no longer offers a free geocoding API.
+
+* **API key**: requires OAuth consumer key and secret (set `Geocoder.configure(:api_key => [key, secret])`)
+* **Key signup**: http://developer.yahoo.com/boss/geo/
+* **Quota**: unlimited, but subject to usage fees
+* **Region**: world
+* **SSL support**: no
+* **Languages**: en, fr, de, it, es, pt, nl, zh, ja, ko
+* **Documentation**: http://developer.yahoo.com/boss/geo/docs/index.html
+* **Terms of Service**: http://info.yahoo.com/legal/us/yahoo/boss/tou/?pir=ucJPcJ1ibUn.h.d.lVmlcbcEkoHjwJ_PvxG9SLK9VIbIQAw1XFrnDqY-
+* **Limitations**: No mass downloads, no commercial map production based on the data, no storage of data except for caching.
+
+#### Bing (`:bing`)
+
+* **API key**: required
+* **Key signup**: http://www.bingmapsportal.com
+* **Quota**: 50,000 requests/24 hrs
+* **Region**: world
+* **SSL support**: no
+* **Languages**: ?
+* **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."
+
+#### Nominatim (`:nominatim`)
+
+* **API key**: none
+* **Quota**: 1 request/second
+* **Region**: world
+* **SSL support**: no
+* **Languages**: ?
+* **Documentation**: http://wiki.openstreetmap.org/wiki/Nominatim
+* **Terms of Service**: http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy
+* **Limitations**: Please limit request rate to 1 per second and include your contact information in User-Agent headers. Data licensed under CC-BY-SA (you must provide attribution).
+
+#### Yandex (`:yandex`)
+
+* **API key**: none
+* **Quota**: 25000 requests / day
+* **Region**: world
+* **SSL support**: no
+* **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**: ?
+
+#### Geocoder.ca (`:geocoder_ca`)
+
+* **API key**: none
+* **Quota**: ?
+* **Region**: US and Canada
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: ?
+* **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."
+
+#### Mapquest (`:mapquest`)
+
+* **API key**: required for the licensed API, do not use for open tier
+* **Quota**: ?
+* **HTTP Headers**: in order to use the licensed API you can configure the http_headers to include a referer as so:
+    `Geocoder.configure(:http_headers => { "Referer" => "http://foo.com" })`
+  You can also allow a blank referer from the API management console via mapquest but it is potentially a security risk that someone else could use your API key from another domain.
+* **Region**: world
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: http://www.mapquestapi.com/geocoding/
+* **Terms of Service**: http://info.mapquest.com/terms-of-use/
+* **Limitations**: ?
+
+#### FreeGeoIP (`:freegeoip`)
+
+* **API key**: none
+* **Quota**: 1000 requests per hour.  After reaching the hourly quota, all of your requests will result in HTTP 403 (Forbidden) until it clears up on the next roll over.
+* **Region**: world
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: http://github.com/fiorix/freegeoip/blob/master/README.rst
+* **Terms of Service**: ?
+* **Limitations**: ?
+
+#### MaxMind Web Services (`:maxmind`)
+
+* **API key**: required
+* **Quota**: Request Packs can be purchased
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: http://www.maxmind.com/app/web_services
+* **Terms of Service**: ?
+* **Limitations**: ?
+
+
+Caching
+-------
+
+It's a good idea, when relying on any external service, 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)
+
+This example uses Redis, but the cache store can be any object that supports these methods:
+
+* `store#[](key)`         - retrieves a value
+* `store#[]=(key, value)` - stores a value
+* `store#keys`            - lists all keys
+* `store#del(url)`        - deletes a value
+
+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).
+
+You can also set a custom prefix to be used for cache keys:
+
+    Geocoder.configure(:cache_prefix => "...")
+
+By default the prefix is `geocoder:`
+
+If you need to expire cached content:
+
+    Geocoder.cache.expire("http://...") # expire cached result for a URL
+    Geocoder.cache.expire(:all)         # expire all cached results
+
+Do *not* include the prefix when passing a URL to be expired. Expiring `:all` will only expire keys with the configured prefix (won't kill every entry in your key/value store).
+
+For an example of a cache store with URL expiry please see examples/autoexpire_cache.rb
+
+_Before you implement caching in your app please be sure that doing so does not violate the Terms of Service for your geocoding service._
+
+
+Forward and Reverse Geocoding in the Same Model
+-----------------------------------------------
+
+If you apply both forward and reverse geocoding functionality to the same model (say users can supply an address or coordinates and you want to fill in whatever's missing), you will provide two address methods:
+
+* one for storing the fetched address (reverse geocoding)
+* one for providing an address to use when fetching coordinates (forward geocoding)
+
+For example:
+
+    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
+
+However, there can be only one set of latitude/longitude attributes, and whichever you specify last will be used. For example:
+
+    class Venue
+
+      geocoded_by :address,
+        :latitude  => :fetched_latitude,  # this will be overridden by the below
+        :longitude => :fetched_longitude  # same here
+
+      reverse_geocoded_by :latitude, :longitude
+    end
+
+The reason for this is that we don't want ambiguity when doing distance calculations. We need a single, authoritative source for coordinates!
+
+
+Use Outside of Rails
+--------------------
+
+You can use Geocoder outside of Rails by calling the `Geocoder.search` method:
+
+    results = Geocoder.search("McCarren Park, Brooklyn, NY")
+
+This returns an array of `Geocoder::Result` objects with all data provided by the geocoding service.
+
+
+Testing Apps that Use Geocoder
+------------------------------
+
+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 and use the `:test` lookup. For example:
+
+    Geocoder.configure(:lookup => :test)
+
+    Geocoder::Lookup::Test.add_stub(
+      "New York, NY", [
+        {
+          'latitude'     => 40.7143528,
+          'longitude'    => -74.0059731,
+          'address'      => 'New York, NY, USA',
+          'state'        => 'New York',
+          'state_code'   => 'NY',
+          'country'      => 'United States',
+          'country_code' => 'US'
+        }
+      ]
+    )
+
+Now, any time Geocoder looks up "New York, NY" its results array will contain one result with the above attributes.
+
+
+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
+    Google 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 reponse, and more. Please run `geocode -h` for details.
+
+Notes on MongoDB
+----------------
+
+### The Near Method
+
+Mongo document classes (Mongoid and MongoMapper) have a built-in `near` scope, but since it only works two-dimensions Geocoder overrides it with its own spherical `near` method in geocoded classes.
+
+### Latitude/Longitude Order
+
+Coordinates are generally printed and spoken as latitude, then longitude ([lat,lon]). Geocoder respects this convention and always expects method arguments to be given in [lat,lon] order. However, MongoDB requires that coordinates be stored in [lon,lat] order as per the GeoJSON spec (http://geojson.org/geojson-spec.html#positions), so internally they are stored "backwards." However, this does not affect order of arguments to methods when using Mongoid or MongoMapper.
+
+To access an object's coordinates in the conventional order, use the `to_coordinates` instance method provided by Geocoder. For example:
+
+    obj.to_coordinates  # => [37.7941013, -122.3951096] # [lat, lon]
+
+Calling `obj.coordinates` directly returns the internal representation of the coordinates which, in the case of MongoDB, is probably the reverse of what you want:
+
+    obj.coordinates     # => [-122.3951096, 37.7941013] # [lon, lat]
+
+For consistency with the rest of Geocoder, always use the `to_coordinates` method instead.
+
+Notes on Non-Rails Frameworks
+-----------------------------
+
+If you are using 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 
+
+Optimisation of Distance Queries
+--------------------------------
+
+In MySQL and Postgres the finding of objects near a given point is speeded up by using a bounding box to limit the number of points over which a full 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]
+
+
+Distance Queries in SQLite
+--------------------------
+
+SQLite's lack of trigonometric functions requires an alternate implementation of the `near` scope. When using SQLite, Geocoder will automatically use a less accurate algorithm for finding objects near a given point. Results of this algorithm should not be trusted too much as it will return objects that are outside the given radius, along with inaccurate distance and bearing calculations.
+
+
+### Discussion
+
+There are few options for finding objects near a given point in SQLite without installing extensions:
+
+1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius), but you will get all the points within the required radius.
+
+2. Load all objects into memory and compute distances between them using the `Geocoder::Calculations.distance_between` method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
+
+3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using `Geocoder::Calculations.distance_between`.
+
+Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.
+
+
+Tests
+-----
+
+Geocoder comes with a test suite (just run `rake test`) that mocks ActiveRecord and is focused on testing the aspects of Geocoder that do not involve executing database queries. Geocoder uses many database engine-specific queries which must be tested against all supported databases (SQLite, MySQL, etc). Ideally this involves creating a full, working Rails application, and that seems beyond the scope of the included test suite. As such, I have created a separate repository which includes a full-blown Rails application and some utilities for easily running tests against multiple environments:
+
+http://github.com/alexreisner/geocoder_test
+
+
+Error Handling
+--------------
+
+By default Geocoder will rescue any exceptions raised by calls to a geocoding service and return an empty array (using warn() to inform you of the error). 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, TimeoutError])
+
+You can also do this to raise all exceptions:
+
+    Geocoder.configure(:always_raise => :all)
+
+The raise-able exceptions are:
+
+    SocketError
+    TimeoutError
+    Geocoder::OverQueryLimitError
+    Geocoder::RequestDenied
+    Geocoder::InvalidRequest
+    Geocoder::InvalidApiKey
+
+Note that not all lookups support all exceptions.
+
+
+Troubleshooting
+---------------
+
+### Mongoid
+
+If you get one of these errors:
+
+    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.
+
+### ActiveRecord
+
+A lot of debugging time can be saved by understanding how Geocoder works with ActiveRecord. When you use the `near` scope or the `nearbys` method of a geocoded object, Geocoder creates an ActiveModel::Relation object which adds some attributes (eg: distance, bearing) to the SELECT clause. It also adds a condition to the WHERE clause to check that distance is within the given radius. Because the SELECT clause is modified, anything else that modifies the SELECT clause may produce strange results, for example:
+
+* using the `pluck` method (selects only a single column)
+* specifying another model through `includes` (selects columns from other tables)
+
+
+Known Issue
+-----------
+
+You cannot use the `near` scope with another scope that provides an `includes` option because the `SELECT` clause generated by `near` will overwrite it (or vice versa). Instead, try using `joins` and pass a `:select` option to the `near` scope to get the columns you want. For example:
+
+    # instead of City.near(...).includes(:venues)
+    City.near("Omaha, NE", 20, :select => "cities.*, venues.*").joins(:venues)
+
+If anyone has a more elegant solution to this problem I am very interested in seeing it.
+
+
+Copyright (c) 2009-12 Alex Reisner, released under the MIT license
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index 883788e5d89a199436bd6e90be123ce9f86b1a77..0000000000000000000000000000000000000000
--- a/README.rdoc
+++ /dev/null
@@ -1,494 +0,0 @@
-= Geocoder
-
-Geocoder is a complete geocoding solution for Ruby. With Rails it adds geocoding (by street or IP address), reverse geocoding (find street address based on given coordinates), and distance queries. It's as simple as calling +geocode+ on your objects, and then using a scope like <tt>Venue.near("Billings, MT")</tt>.
-
-
-== Compatibility
-
-* Supports multiple Ruby versions: Ruby 1.8.7, 1.9.2, and JRuby.
-* Supports multiple databases: MySQL, PostgreSQL, SQLite, and MongoDB (1.7.0 and higher).
-* Supports Rails 3. If you need to use it with Rails 2 please see the <tt>rails2</tt> 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.
-
-
-== Install
-
-=== As a Gem
-
-Add to your Gemfile:
-
-  gem "geocoder"
-
-and run at the command prompt:
-
-  bundle install
-
-=== Or As a Plugin
-
-At the command prompt:
-
-  rails plugin install git://github.com/alexreisner/geocoder.git
-
-
-== Configure Object Geocoding
-
-In the below, note that addresses may be street or IP addresses.
-
-=== ActiveRecord
-
-Your model must have two attributes (database columns) for storing latitude and longitude coordinates. By default they should be called +latitude+ and +longitude+ but this can be changed (see "More on Configuration" below):
-
-  rails generate migration AddLatitudeAndLongitudeToModel latitude:float longitude:float
-  rake db:migrate
-
-For reverse geocoding your model must provide a method that returns an address. 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+).
-
-Next, your model must tell Geocoder which method returns your object's geocodable address:
-
-  geocoded_by :full_street_address   # can also be an IP address
-  after_validation :geocode          # auto-fetch coordinates
-
-For reverse geocoding, tell Geocoder which attributes store latitude and longitude:
-
-  reverse_geocoded_by :latitude, :longitude
-  after_validation :reverse_geocode  # auto-fetch address
-
-=== Mongoid
-
-First, your model must have an array field for storing coordinates:
-
-  field :coordinates, :type => Array
-
-You may also want an address field, like this:
-
-  field :address
-
-but if you store address components (city, state, country, etc) in separate fields you can instead define a method called +address+ that combines them into a single string which will be used to query the geocoding service.
-
-Once your fields are defined, include the <tt>Geocoder::Model::Mongoid</tt> module and then call <tt>geocoded_by</tt>:
-
-  include Geocoder::Model::Mongoid
-  geocoded_by :address               # can also be an IP address
-  after_validation :geocode          # auto-fetch coordinates
-
-Reverse geocoding is similar:
-
-  include Geocoder::Model::Mongoid
-  reverse_geocoded_by :coordinates
-  after_validation :reverse_geocode  # auto-fetch address
-
-Be sure to read <i>Latitude/Longitude Order</i> in the <i>Notes on MongoDB</i> section below on how to properly retrieve latitude/longitude coordinates from your objects.
-
-=== MongoMapper
-
-MongoMapper is very similar to Mongoid, just be sure to include <tt>Geocoder::Model::MongoMapper</tt>.
-
-=== Bulk 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
-
-Geocoder will print warnings if you exceed the rate limit for your geocoding service.
-
-
-== Request Geocoding by IP Address
-
-Geocoder adds a +location+ method to the standard <tt>Rack::Request</tt> 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
-
-See "Advanced Geocoding" below for more information about Geocoder::Result objects.
-
-
-== Location-Aware Database Queries
-
-To find objects by location, use the following scopes:
-
-  Venue.near('Omaha, NE, US', 20)    # venues within 20 miles of Omaha
-  Venue.near([40.71, 100.23], 20)    # venues within 20 miles of a point
-  Venue.geocoded                     # venues with coordinates
-  Venue.not_geocoded                 # venues without coordinates
-
-With geocoded objects you can do things like this:
-
-  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
-
-Some utility methods are also available:
-
-  # look up coordinates of some location (like searching Google Maps)
-  Geocoder.coordinates("25 Main St, Cooperstown, NY")
-   => [42.700149, -74.922767]
-
-  # distance (in miles) between Eiffel Tower and Empire State Building
-  Geocoder::Calculations.distance_between([47.858205,2.294359], [40.748433,-73.985655])
-   => 3619.77359999382
-
-  # 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]
-
-Please see the code for more methods and detailed information about arguments (eg, working with kilometers).
-
-
-== Distance and Bearing
-
-When you run a location-aware query the returned objects have two attributes added to them (only w/ ActiveRecord):
-
-* <tt>obj.distance</tt> - number of miles from the search point to this object
-* <tt>obj.bearing</tt> - direction from the search point to this object
-
-Results are automatically sorted by distance from the search point, closest to farthest. Bearing is given as a number of clockwise degrees from due north, for example:
-
-* <tt>0</tt> - due north
-* <tt>180</tt> - due south
-* <tt>90</tt> - due east
-* <tt>270</tt> - due west
-* <tt>230.1</tt> - southwest
-* <tt>359.9</tt> - almost due north
-
-You can convert these numbers to compass point names by using the utility method provided:
-
-  Geocoder::Calculations.compass_point(355) # => "N"
-  Geocoder::Calculations.compass_point(45)  # => "NE"
-  Geocoder::Calculations.compass_point(208) # => "SW"
-
-<i>Note: when using SQLite +distance+ and +bearing+ values are provided for interface consistency only. They are not very accurate.</i>
-
-To calculate accurate distance and bearing with SQLite or MongoDB:
-
-  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 <tt>bearing_from/to</tt> methods take a single argument which can be: a <tt>[lat,lon]</tt> array, a geocoded object, or a geocodable address (string). The <tt>distance_from/to</tt> methods also take a units argument (<tt>:mi</tt> or <tt>:km</tt>).
-
-
-== More on Configuration
-
-You are not stuck with using 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
-
-The +address+ method can return any string you'd use to search Google Maps. For example, any of the following are acceptable:
-
-* "714 Green St, Big Town, MO"
-* "Eiffel Tower, Paris, FR"
-* "Paris, TX, US"
-
-If your model has +street+, +city+, +state+, and +country+ attributes you might do something like this:
-
-  geocoded_by :address
-
-  def address
-    [street, city, state, country].compact.join(', ')
-  end
-
-For reverse geocoding you can also specify an alternate name attribute where the address will be stored, for example:
-
-  reverse_geocoded_by :latitude, :longitude, :address => :location  # ActiveRecord
-  reverse_geocoded_by :coordinates, :address => :loc     # MongoDB
-
-
-== Advanced Geocoding
-
-So far we have looked at shortcuts for assigning geocoding results to object attributes. However, if you need to do something fancy you can skip the auto-assignment by providing a block (takes the object to be geocoded and an array of <tt>Geocoder::Result</tt> objects) in which you handle the parsed geocoding result 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
-
-Every <tt>Geocoder::Result</tt> object, +result+, provides the following data:
-
-* <tt>result.latitude</tt> - float
-* <tt>result.longitude</tt> - float
-* <tt>result.coordinates</tt> - array of the above two
-* <tt>result.address</tt> - string
-* <tt>result.city</tt> - string
-* <tt>result.state</tt> - string
-* <tt>result.state_code</tt> - string
-* <tt>result.postal_code</tt> - string
-* <tt>result.country</tt> - string
-* <tt>result.country_code</tt> - string
-
-If you're familiar with the results returned by the geocoding service you're using you can access even more data, but you'll need to be familiar with the particular <tt>Geocoder::Result</tt> object you're using and the structure of your geocoding service's responses. (See below for links to geocoding service documentation.)
-
-
-== Geocoding Services
-
-By default Geocoder uses Google's geocoding API to fetch coordinates and street addresses (FreeGeoIP is used for IP address info). However there are several other APIs supported, as well as a variety of settings. Please see the listing and comparison below for details on specific geocoding services (not all settings are supported by all services). The configuration options are:
-
-  # config/initializers/geocoder.rb
-
-  # geocoding service (see below for supported options):
-  Geocoder::Configuration.lookup = :yahoo
-
-  # to use an API key:
-  Geocoder::Configuration.api_key = "..."
-
-  # geocoding service request timeout, in seconds (default 3):
-  Geocoder::Configuration.timeout = 5
-
-  # use HTTPS for geocoding service connections:
-  Geocoder::Configuration.use_https = true
-
-  # language to use (for search queries and reverse geocoding):
-  Geocoder::Configuration.language = :de
-
-  # use a proxy to access the service:
-  Geocoder::Configuration.http_proxy  = "127.4.4.1"
-  Geocoder::Configuration.https_proxy = "127.4.4.2" # only if HTTPS is needed
-
-  # caching (see below for details)
-  Geocoder::Configuration.cache = Redis.new
-  Geocoder::Configuration.cache_prefix = "..."
-
-
-=== Listing and Comparison
-
-The following is a comparison of the supported geocoding APIs. The "Limitations" listed for each are a very brief and incomplete summary of some special limitations beyond basic data source attribution. Please read the official Terms of Service for a service before using it.
-
-==== Google (<tt>:google</tt>)
-
-API key:: optional (required for Premier)
-Key signup:: http://code.google.com/apis/maps/signup.html
-Quota:: 2,500 requests/day, 100,000 with Google Maps API Premier
-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)
-Documentation:: http://code.google.com/apis/maps/documentation/geocoding/#JSON
-Terms of Service:: http://code.google.com/apis/maps/terms.html#section_10_12
-Limitations:: "You must not use or display the Content without a corresponding Google map, unless you are explicitly permitted to do so in the Maps APIs Documentation, or through written permission from Google." "You must not pre-fetch, cache, or store any Content, except that you may store: (i) limited amounts of Content for the purpose of improving the performance of your Maps API Implementation..."
-Notes:: To use Google Premier set <tt>Geocoder::Configuration.lookup = :google_premier</tt> and <tt>Geocoder::Configuration.api_key = [key, client, channel]</tt>.
-
-==== Yahoo (<tt>:yahoo</tt>)
-
-API key:: optional in development (required for production apps)
-Key signup:: https://developer.apps.yahoo.com/wsregapp
-Quota:: 50,000 requests/day, more available by special arrangement
-Region:: world
-SSL support:: no
-Languages:: ?
-Documentation:: http://developer.yahoo.com/geo/placefinder/guide/responses.html
-Terms of Service:: http://info.yahoo.com/legal/us/yahoo/maps/mapsapi/mapsapi-2141.html
-Limitations:: "YOU SHALL NOT... (viii) store or allow end users to store map imagery, map data or geocoded location information from the Yahoo! Maps APIs for any future use; (ix) use the stand-alone geocoder for any use other than displaying Yahoo! Maps or displaying points on Yahoo! Maps;"
-
-==== Bing (<tt>:bing</tt>)
-
-API key:: required
-Key signup:: http://www.bingmapsportal.com
-Quota:: 50,000 requests/24 hrs
-Region:: world
-SSL support:: no
-Languages:: ?
-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."
-
-==== Nominatim (<tt>:nominatim</tt>)
-
-API key:: none
-Quota:: 1 request/second
-Region:: world
-SSL support:: no
-Languages:: ?
-Documentation:: http://wiki.openstreetmap.org/wiki/Nominatim
-Terms of Service:: http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy
-Limitations:: Please limit request rate to 1 per second and include your contact information in User-Agent headers. Data licensed under CC-BY-SA (you must provide attribution).
-
-==== Yandex (<tt>:yandex</tt>)
-
-API key:: required
-Key signup:: http://api.yandex.ru/maps/intro/concepts/intro.xml#apikey
-Quota:: ?
-Region:: Russia
-SSL support:: no
-Languages:: Russian, Belarusian, and Ukrainian
-Documentation:: http://api.yandex.ru/maps/geocoder/doc/desc/concepts/response_structure.xml
-Terms of Service:: http://api.yandex.com/direct/eula.xml?ncrnd=8453
-Limitations:: ?
-
-==== Geocoder.ca (<tt>:geocoder_ca</tt>)
-
-API key:: none
-Quota:: ?
-Region:: US and Canada
-SSL support:: no
-Languages:: English
-Documentation:: ?
-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."
-
-==== FreeGeoIP
-
-API key:: none
-Quota:: ?
-Region:: world
-SSL support:: no
-Languages:: English
-Documentation:: http://github.com/fiorix/freegeoip/blob/master/README.rst
-Terms of Service:: ?
-Limitations:: ?
-
-
-== Caching
-
-It's a good idea, when relying on any external service, 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::Configuration.cache = Redis.new
-
-This example uses Redis, but the cache store can be any object that supports these methods:
-
-* <tt>store#[](key)</tt>         - retrieves a value
-* <tt>store#[]=(key, value)</tt> - stores a value
-* <tt>store#keys</tt>            - lists all keys
-
-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).
-
-You can also set a custom prefix to be used for cache keys:
-
-  Geocoder::Configuration.cache_prefix = "..."
-
-By default the prefix is <tt>geocoder:</tt>
-
-If you need to expire cached content:
-
-  Geocoder.cache.expire("http://...") # expire cached result for a URL
-  Geocoder.cache.expire(:all)         # expire all cached results
-
-Do *not* include the prefix when passing a URL to be expired. Expiring <tt>:all</tt> will only expire keys with the configured prefix (won't kill every entry in your key/value store).
-
-<i>Before you implement caching in your app please be sure that doing so does not violate the Terms of Service for your geocoding service.</i>
-
-
-== Forward and Reverse Geocoding in the Same Model
-
-If you apply both forward and reverse geocoding functionality to the same model (say users can supply an address or coordinates and you want to fill in whatever's missing), you will provide two address methods:
-
-* one for storing the fetched address (reverse geocoding)
-* one for providing an address to use when fetching coordinates (forward geocoding)
-
-For example:
-
-  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
-
-However, there can be only one set of latitude/longitude attributes, and whichever you specify last will be used. For example:
-
-  class Venue
-
-    geocoded_by :address,
-      :latitude  => :fetched_latitude,  # this will be overridden by the below
-      :longitude => :fetched_longitude  # same here
-
-    reverse_geocoded_by :latitude, :longitude
-  end
-
-The reason for this is that we don't want ambiguity when doing distance calculations. We need a single, authoritative source for coordinates!
-
-
-== Use Outside of Rails
-
-You can use Geocoder outside of Rails by calling the <tt>Geocoder.search</tt> method:
-
-  results = Geocoder.search("McCarren Park, Brooklyn, NY")
-
-This returns an array of <tt>Geocoder::Result</tt> objects with all information provided by the geocoding service. Please see above and in the code for details.
-
-
-== 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
-  Google 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 reponse, and more. Please run <tt>geocode -h</tt> for details.
-
-
-== Notes on MongoDB
-
-=== The Near Method
-
-Mongo document classes (Mongoid and MongoMapper) have a built-in +near+ scope, but since it only works two-dimensions Geocoder overrides it with its own spherical +near+ method in geocoded classes.
-
-=== Latitude/Longitude Order
-
-Coordinates are generally printed and spoken as latitude, then logitude ([lat,lon]). Geocoder respects this convention and always expects method arguments to be given in [lat,lon] order. However, MongoDB requires that coordinates be stored in [lon,lat] order as per the GeoJSON spec (http://geojson.org/geojson-spec.html#positions), so internally they are stored "backwards." However, this does not affect order of arguments to methods when using Mongoid or MongoMapper.
-
-To access an object's coordinates in the conventional order, use the <tt>to_coordinates</tt> instance method provided by Geocoder. For example:
-
-    obj.to_coordinates  # => [37.7941013, -122.3951096] # [lat, lon]
-
-Calling <tt>obj.coordinates</tt> directly returns the internal representation of the coordinates which, in the case of MongoDB, is probably the reverse of what you want:
-
-    obj.coordinates     # => [-122.3951096, 37.7941013] # [lon, lat]
-
-For consistency with the rest of Geocoder, always use the <tt>to_coordinates</tt> method instead.
-
-
-== Distance Queries in SQLite
-
-SQLite's lack of trigonometric functions requires an alternate implementation of the +near+ scope. When using SQLite, Geocoder will automatically use a less accurate algorithm for finding objects near a given point. Results of this algorithm should not be trusted too much as it will return objects that are outside the given radius, along with inaccurate distance and bearing calculations.
-
-
-=== Discussion
-
-There are few options for finding objects near a given point in SQLite without installing extensions:
-
-1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius), but you will get all the points within the required radius.
-
-2. Load all objects into memory and compute distances between them using the <tt>Geocoder::Calculations.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
-
-3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder::Calculations.distance_between</tt>.
-
-Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.
-
-
-== Tests
-
-Geocoder comes with a test suite (just run <tt>rake test</tt>) that mocks ActiveRecord and is focused on testing the aspects of Geocoder that do not involve executing database queries. Geocoder uses many database engine-specific queries which must be tested against all supported databases (SQLite, MySQL, etc). Ideally this involves creating a full, working Rails application, and that seems beyond the scope of the included test suite. As such, I have created a separate repository which includes a full-blown Rails application and some utilities for easily running tests against multiple environments:
-
-http://github.com/alexreisner/geocoder_test
-
-
-== Error Handling
-
-By default Geocoder will rescue any exceptions raised by calls to the geocoding service and return an empty array (using warn() to inform you of the error). You can override this and implement custom error handling for certain exceptions by using the <tt>:always_raise</tt> option:
-
-  Geocoder::Configuration.always_raise = [SocketError, TimeoutError]
-
-
-== Known Issue
-
-You cannot use the +near+ scope with another scope that provides an +includes+ option because the +SELECT+ clause generated by +near+ will overwrite it (or vice versa). Instead, try using +joins+ and pass a <tt>:select</tt> option to the +near+ scope to get the columns you want. For example:
-
-  # instead of City.near(...).includes(:venues)
-  City.near("Omaha, NE", 20, :select => "cities.*, venues.*").joins(:venues)
-
-If anyone has a more elegant solution to this problem I am very interested in seeing it.
-
-
-Copyright (c) 2009-11 Alex Reisner, released under the MIT license
diff --git a/examples/autoexpire_cache.rb b/examples/autoexpire_cache.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ced862b3d875a81bf701d4dc5356e3587382371a
--- /dev/null
+++ b/examples/autoexpire_cache.rb
@@ -0,0 +1,28 @@
+# 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 AutoexpireCache
+  def initialize(store)
+    @store = store
+    @ttl = 86400
+  end
+
+  def [](url)
+    @store.[](url)
+  end
+
+  def []=(url, value)
+    @store.[]=(url, value)
+    @store.expire(url, @ttl)
+  end
+
+  def keys
+    @store.keys
+  end
+
+  def del(url)
+    @store.del(url)
+  end
+end
+
+Geocoder.configure(:cache => AutoexpireCache.new(Redis.new))
diff --git a/gemfiles/Gemfile.mongoid-2.4.x b/gemfiles/Gemfile.mongoid-2.4.x
new file mode 100644
index 0000000000000000000000000000000000000000..e3fe031df45723a395a423308638b979b3ccac30
--- /dev/null
+++ b/gemfiles/Gemfile.mongoid-2.4.x
@@ -0,0 +1,15 @@
+source "http://rubygems.org"
+
+gemspec :path => '..'
+
+group :development, :test do
+  gem 'rake'
+  gem 'mongoid', '2.4.11'
+  gem 'bson_ext', :platforms => :ruby
+
+  gem 'rails'
+
+  platforms :jruby do
+    gem 'jruby-openssl'
+  end
+end
diff --git a/lib/generators/geocoder/config/config_generator.rb b/lib/generators/geocoder/config/config_generator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3fe3b3c17fecaeb026c87013e0a63f24c05333a8
--- /dev/null
+++ b/lib/generators/geocoder/config/config_generator.rb
@@ -0,0 +1,14 @@
+require 'rails/generators'
+
+module Geocoder
+  class ConfigGenerator < Rails::Generators::Base
+    source_root File.expand_path("../templates", __FILE__)
+
+    desc "This generator creates an initializer file at config/initializers, " +
+         "with the default configuration options for Geocoder."
+    def add_initializer
+      template "initializer.rb", "config/initializers/geocoder.rb"
+    end
+  end
+end
+
diff --git a/lib/generators/geocoder/config/templates/initializer.rb b/lib/generators/geocoder/config/templates/initializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ef241091677e92282527e3d31c84d905fe133490
--- /dev/null
+++ b/lib/generators/geocoder/config/templates/initializer.rb
@@ -0,0 +1,21 @@
+Geocoder.configure(
+  # geocoding options
+  # :timeout      => 3,           # geocoding service timeout (secs)
+  # :lookup       => :google,     # name of geocoding service (symbol)
+  # :language     => :en,         # ISO-639 language code
+  # :use_https    => false,       # use HTTPS for lookup requests? (if supported)
+  # :http_proxy   => nil,         # HTTP proxy server (user:pass@host:port)
+  # :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 #keys)
+  # :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);
+  # supports SocketError and TimeoutError
+  # :always_raise => [],
+
+  # calculation options
+  # :units     => :mi,       # :km for kilometers or :mi for miles
+  # :distances => :linear    # :spherical or :linear
+)
diff --git a/lib/geocoder.rb b/lib/geocoder.rb
index a4b52ac55bab3f091cec83fc9d649a322d848fed..b3bae2bff6f1899aff6b258e4d6b70612b959555 100644
--- a/lib/geocoder.rb
+++ b/lib/geocoder.rb
@@ -1,11 +1,13 @@
 require "geocoder/configuration"
+require "geocoder/query"
 require "geocoder/calculations"
 require "geocoder/exceptions"
 require "geocoder/cache"
 require "geocoder/request"
-require "geocoder/models/active_record"
-require "geocoder/models/mongoid"
-require "geocoder/models/mongo_mapper"
+require "geocoder/lookup"
+require "geocoder/models/active_record" if defined?(::ActiveRecord)
+require "geocoder/models/mongoid" if defined?(::Mongoid)
+require "geocoder/models/mongo_mapper" if defined?(::MongoMapper)
 
 module Geocoder
   extend self
@@ -13,15 +15,16 @@ module Geocoder
   ##
   # Search for information about an address or a set of coordinates.
   #
-  def search(query)
-    blank_query?(query) ? [] : lookup(query).search(query)
+  def search(query, options = {})
+    query = Geocoder::Query.new(query, options) unless query.is_a?(Geocoder::Query)
+    query.blank? ? [] : query.execute
   end
 
   ##
   # Look up the coordinates of the given street or IP address.
   #
-  def coordinates(address)
-    if (results = search(address)).size > 0
+  def coordinates(address, options = {})
+    if (results = search(address, options)).size > 0
       results.first.coordinates
     end
   end
@@ -30,8 +33,8 @@ module Geocoder
   # Look up the address of the given coordinates ([lat,lon])
   # or IP address (string).
   #
-  def address(query)
-    if (results = search(query)).size > 0
+  def address(query, options = {})
+    if (results = search(query, options)).size > 0
       results.first.address
     end
   end
@@ -40,89 +43,8 @@ module Geocoder
   # The working Cache object, or +nil+ if none configured.
   #
   def cache
-    if @cache.nil? and store = Configuration.cache
-      @cache = Cache.new(store, Configuration.cache_prefix)
-    end
-    @cache
-  end
-
-  ##
-  # Array of valid Lookup names.
-  #
-  def valid_lookups
-    street_lookups + ip_lookups
-  end
-
-  ##
-  # All street address lookups, default first.
-  #
-  def street_lookups
-    [:google, :google_premier, :yahoo, :bing, :geocoder_ca, :yandex, :nominatim]
-  end
-
-  ##
-  # All IP address lookups, default first.
-  #
-  def ip_lookups
-    [:freegeoip]
-  end
-
-
-  private # -----------------------------------------------------------------
-
-  ##
-  # Get a Lookup object (which communicates with the remote geocoding API).
-  # Takes a search query and returns an IP or street address Lookup
-  # depending on the query contents.
-  #
-  def lookup(query)
-    if ip_address?(query)
-      get_lookup(ip_lookups.first)
-    else
-      get_lookup(Configuration.lookup || street_lookups.first)
-    end
-  end
-
-  ##
-  # Retrieve a Lookup object from the store.
-  #
-  def get_lookup(name)
-    @lookups = {} unless defined?(@lookups)
-    @lookups[name] = spawn_lookup(name) unless @lookups.include?(name)
-    @lookups[name]
-  end
-
-  ##
-  # Spawn a Lookup of the given name.
-  #
-  def spawn_lookup(name)
-    if valid_lookups.include?(name)
-      name = name.to_s
-      require "geocoder/lookups/#{name}"
-      klass = name.split("_").map{ |i| i[0...1].upcase + i[1..-1] }.join
-      Geocoder::Lookup.const_get(klass).new
-    else
-      valids = valid_lookups.map(&:inspect).join(", ")
-      raise ConfigurationError, "Please specify a valid lookup for Geocoder " +
-        "(#{name.inspect} is not one of: #{valids})."
-    end
-  end
-
-  ##
-  # Does the given value look like an IP address?
-  #
-  # Does not check for actual validity, just the appearance of four
-  # dot-delimited numbers.
-  #
-  def ip_address?(value)
-    !!value.to_s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
-  end
-
-  ##
-  # Is the given search query blank? (ie, should we not bother searching?)
-  #
-  def blank_query?(value)
-    !!value.to_s.match(/^\s*$/)
+    warn "WARNING: Calling Geocoder.cache is DEPRECATED. The #cache method now belongs to the Geocoder::Lookup object."
+    Geocoder::Lookup.get(Geocoder.config.lookup).send(:configuration).cache
   end
 end
 
diff --git a/lib/geocoder/cache.rb b/lib/geocoder/cache.rb
index 2465d29f43edb82c504fce6bf202387099fb6720..6218db5fd94c676ef856ea87e06950377445a06e 100644
--- a/lib/geocoder/cache.rb
+++ b/lib/geocoder/cache.rb
@@ -10,14 +10,24 @@ module Geocoder
     # Read from the Cache.
     #
     def [](url)
-      interpret store[key_for(url)]
+      interpret case
+        when store.respond_to?(:[])
+          store[key_for(url)]
+        when store.respond_to?(:get)
+          store.get key_for(url)
+      end
     end
 
     ##
     # Write to the Cache.
     #
     def []=(url, value)
-      store[key_for(url)] = value
+      case
+        when store.respond_to?(:[]=)
+          store[key_for(url)] = value
+        when store.respond_to?(:set)
+          store.set key_for(url), value
+      end
     end
 
     ##
@@ -68,7 +78,8 @@ module Geocoder
     end
 
     def expire_single_url(url)
-      store.del(key_for(url))
+      key = key_for(url)
+      store.respond_to?(:del) ? store.del(key) : store.delete(key)
     end
   end
 end
diff --git a/lib/geocoder/calculations.rb b/lib/geocoder/calculations.rb
index abc8b94740fdc7bd4a6f080ce4d4d956a14d4063..70c8fcb350fd90732aee1c2bea9b9ece85692c62 100644
--- a/lib/geocoder/calculations.rb
+++ b/lib/geocoder/calculations.rb
@@ -21,10 +21,26 @@ module Geocoder
     #
     KM_IN_MI = 0.621371192
 
+    # Not a number constant
+    NAN = defined?(::Float::NAN) ? ::Float::NAN : 0 / 0.0
+
+    ##
+    # Returns true if all given arguments are valid latitude/longitude values.
+    #
+    def coordinates_present?(*args)
+      args.each do |a|
+        # note that Float::NAN != Float::NAN
+        # still, this could probably be improved:
+        return false if (!a.is_a?(Numeric) or a.to_s == "NaN")
+      end
+      true
+    end
+
     ##
     # Distance spanned by one degree of latitude in the given units.
     #
-    def latitude_degree_distance(units = :mi)
+    def latitude_degree_distance(units = nil)
+      units ||= Geocoder.config.units
       2 * Math::PI * earth_radius(units) / 360
     end
 
@@ -32,7 +48,8 @@ module Geocoder
     # Distance spanned by one degree of longitude at the given latitude.
     # This ranges from around 69 miles at the equator to zero at the poles.
     #
-    def longitude_degree_distance(latitude, units = :mi)
+    def longitude_degree_distance(latitude, units = nil)
+      units ||= Geocoder.config.units
       latitude_degree_distance(units) * Math.cos(to_radians(latitude))
     end
 
@@ -49,12 +66,13 @@ module Geocoder
     #
     # The options hash supports:
     #
-    # * <tt>:units</tt> - <tt>:mi</tt> (default) or <tt>:km</tt>
+    # * <tt>:units</tt> - <tt>:mi</tt> or <tt>:km</tt>
+    #   Use Geocoder.configure(:units => ...) to configure default units.
     #
     def distance_between(point1, point2, options = {})
 
       # set default options
-      options[:units] ||= :mi
+      options[:units] ||= Geocoder.config.units
 
       # convert to coordinate arrays
       point1 = extract_coordinates(point1)
@@ -81,17 +99,18 @@ module Geocoder
     # See Geocoder::Calculations.distance_between for
     # ways of specifying the points. Also accepts an options hash:
     #
-    # * <tt>:method</tt> - <tt>:linear</tt> (default) or <tt>:spherical</tt>;
+    # * <tt>:method</tt> - <tt>:linear</tt> or <tt>:spherical</tt>;
     #   the spherical method is "correct" in that it returns the shortest path
-    #   (one along a great circle) but the linear method is the default as it
-    #   is less confusing (returns due east or west when given two points with
-    #   the same latitude)
+    #   (one along a great circle) but the linear method is less confusing
+    #   (returns due east or west when given two points with the same latitude).
+    #   Use Geocoder.configure(:distances => ...) to configure calculation method.
     #
     # Based on: http://www.movable-type.co.uk/scripts/latlong.html
     #
     def bearing_between(point1, point2, options = {})
 
       # set default options
+      options[:method] ||= Geocoder.config.distances
       options[:method] = :linear unless options[:method] == :spherical
 
       # convert to coordinate arrays
@@ -165,7 +184,7 @@ module Geocoder
     end
 
     ##
-    # Returns coordinates of the lower-left and upper-right corners of a box
+    # Returns coordinates of the southwest and northeast corners of a box
     # with the given point at its center. The radius is the shortest distance
     # from the center point to any side of the box (the length of each side
     # is twice the radius).
@@ -177,12 +196,13 @@ module Geocoder
     # See Geocoder::Calculations.distance_between for
     # ways of specifying the point. Also accepts an options hash:
     #
-    # * <tt>:units</tt> - <tt>:mi</tt> (default) or <tt>:km</tt>
+    # * <tt>:units</tt> - <tt>:mi</tt> or <tt>:km</tt>.
+    #   Use Geocoder.configure(:units => ...) to configure default units.
     #
     def bounding_box(point, radius, options = {})
       lat,lon = extract_coordinates(point)
       radius  = radius.to_f
-      units   = options[:units] || :mi
+      units   = options[:units] || Geocoder.config.units
       [
         lat - (radius / latitude_degree_distance(units)),
         lon - (radius / longitude_degree_distance(lat, units)),
@@ -219,11 +239,13 @@ module Geocoder
       end
     end
 
-    def distance_to_radians(distance, units = :mi)
+    def distance_to_radians(distance, units = nil)
+      units ||= Geocoder.config.units
       distance.to_f / earth_radius(units)
     end
 
-    def radians_to_distance(radians, units = :mi)
+    def radians_to_distance(radians, units = nil)
+      units ||= Geocoder.config.units
       radians * earth_radius(units)
     end
 
@@ -242,9 +264,11 @@ module Geocoder
     end
 
     ##
-    # Radius of the Earth in the given units (:mi or :km). Default is :mi.
+    # Radius of the Earth in the given units (:mi or :km).
+    # Use Geocoder.configure(:units => ...) to configure default units.
     #
-    def earth_radius(units = :mi)
+    def earth_radius(units = nil)
+      units ||= Geocoder.config.units
       units == :km ? EARTH_RADIUS : to_miles(EARTH_RADIUS)
     end
 
@@ -270,10 +294,26 @@ module Geocoder
     #
     def extract_coordinates(point)
       case point
-        when Array; point
-        when String; Geocoder.coordinates(point)
-        else point.to_coordinates
+      when Array
+        if point.size == 2
+          lat, lon = point
+          if !lat.nil? && lat.respond_to?(:to_f) and
+            !lon.nil? && lon.respond_to?(:to_f)
+          then
+            return [ lat.to_f, lon.to_f ]
+          end
+        end
+      when String
+        point = Geocoder.coordinates(point) and return point
+      else
+        if point.respond_to?(:to_coordinates)
+          if Array === array = point.to_coordinates
+            return extract_coordinates(array)
+          end
+        end
       end
+      [ NAN, NAN ]
     end
   end
 end
+
diff --git a/lib/geocoder/cli.rb b/lib/geocoder/cli.rb
index 8822bf627a664ae6cb48ebddaf63f40a91329544..ab3df3b78b2ef6e97a32184212d5862bdec96293 100644
--- a/lib/geocoder/cli.rb
+++ b/lib/geocoder/cli.rb
@@ -13,33 +13,33 @@ module Geocoder
         opts.separator "\nOptions: "
 
         opts.on("-k <key>", "--key <key>",
-          "Key for geocoding API (optional for most). For Google Premier use 'key client channel' separated by spaces") do |key|
-          premier_key = key.split(' ')
-          if premier_key.length == 3
-            Geocoder::Configuration.api_key = premier_key
+          "Key for geocoding API (usually optional). Enclose multi-part keys in quotes and separate parts by spaces") do |key|
+          if (key_parts = key.split(/\s+/)).size > 1
+            Geocoder.configure(:api_key => key_parts)
           else
-            Geocoder::Configuration.api_key = key
+            Geocoder.configure(:api_key => key)
           end
         end
 
         opts.on("-l <language>", "--language <language>",
           "Language of output (see API docs for valid choices)") do |language|
-          Geocoder::Configuration.language = language
+          Geocoder.configure(:language => language)
         end
 
         opts.on("-p <proxy>", "--proxy <proxy>",
           "HTTP proxy server to use (user:pass@host:port)") do |proxy|
-          Geocoder::Configuration.http_proxy = proxy
+          Geocoder.configure(:http_proxy => proxy)
         end
 
-        opts.on("-s <service>", Geocoder.street_lookups, "--service <service>",
-          "Geocoding service: #{Geocoder.street_lookups * ', '}") do |service|
-          Geocoder::Configuration.lookup = service.to_sym
+        opts.on("-s <service>", Geocoder::Lookup.all_services_except_test, "--service <service>",
+          "Geocoding service: #{Geocoder::Lookup.all_services_except_test * ', '}") do |service|
+          Geocoder.configure(:lookup => service.to_sym)
+          Geocoder.configure(:ip_lookup => service.to_sym)
         end
 
         opts.on("-t <seconds>", "--timeout <seconds>",
           "Maximum number of seconds to wait for API response") do |timeout|
-          Geocoder::Configuration.timeout = timeout.to_i
+          Geocoder.configure(:timeout => timeout.to_i)
         end
 
         opts.on("-j", "--json", "Print API's raw JSON response") do
@@ -78,21 +78,19 @@ module Geocoder
       end
 
       if show_url
-        lookup = Geocoder.send(:lookup, query)
-        reverse = lookup.send(:coordinates?, query)
-        out << lookup.send(:query_url, query, reverse) + "\n"
+        q = Geocoder::Query.new(query)
+        out << q.url + "\n"
         exit 0
       end
 
       if show_json
-        lookup = Geocoder.send(:lookup, query)
-        reverse = lookup.send(:coordinates?, query)
-        out << lookup.send(:fetch_raw_data, query, reverse) + "\n"
+        q = Geocoder::Query.new(query)
+        out << q.lookup.send(:fetch_raw_data, q) + "\n"
         exit 0
       end
 
       if (result = Geocoder.search(query).first)
-        lookup = Geocoder.send(:get_lookup, :google)
+        google = Geocoder::Lookup.get(:google)
         lines = [
           ["Latitude",       result.latitude],
           ["Longitude",      result.longitude],
@@ -101,7 +99,7 @@ module Geocoder
           ["State/province", result.state],
           ["Postal code",    result.postal_code],
           ["Country",        result.country],
-          ["Google map",     lookup.map_link_url(result.coordinates)],
+          ["Google map",     google.map_link_url(result.coordinates)],
         ]
         lines.each do |line|
           out << (line[0] + ": ").ljust(18) + line[1].to_s + "\n"
diff --git a/lib/geocoder/configuration.rb b/lib/geocoder/configuration.rb
index 44baabeebf4547d063ee0798ce643a2d1aebfa86..dc5d422280063d9ae513efadd287fe78bb82513d 100644
--- a/lib/geocoder/configuration.rb
+++ b/lib/geocoder/configuration.rb
@@ -1,58 +1,130 @@
-module Geocoder
-  class Configuration
+require 'singleton'
+require 'geocoder/configuration_hash'
 
-    def self.options_and_defaults
-      [
-        # geocoding service timeout (secs)
-        [:timeout, 3],
+module Geocoder
 
-        # name of geocoding service (symbol)
-        [:lookup, :google],
+  ##
+  # Configuration options should be set by passing a hash:
+  #
+  #   Geocoder.configure(
+  #     :timeout  => 5,
+  #     :lookup   => :yandex,
+  #     :api_key  => "2a9fsa983jaslfj982fjasd",
+  #     :units    => :km
+  #   )
+  #
+  def self.configure(options = nil, &block)
+    if block_given?
+      warn "WARNING: Passing a block to Geocoder.configure is DEPRECATED. Please pass a hash instead (eg: Geocoder.configure(:units => ..., :api_key => ...))."
+      block.call(Configuration.instance)
+    elsif !options.nil?
+      Configuration.instance.configure(options)
+    else
+      warn "WARNING: Use of Geocoder.configure to read or write single config options is DEPRECATED. To write to the config please pass a hash (eg: Geocoder.configure(:units => ...)). To read config options please use the Geocoder.config object (eg: Geocoder.config.units)."
+      Configuration.instance
+    end
+  end
 
-        # ISO-639 language code
-        [:language, :en],
+  ##
+  # Read-only access to the singleton's config data.
+  #
+  def self.config
+    Configuration.instance.data
+  end
 
-        # use HTTPS for lookup requests? (if supported)
-        [:use_https, false],
+  ##
+  # Read-only access to lookup-specific config data.
+  #
+  def self.config_for_lookup(lookup_name)
+    data = config.clone
+    data.reject!{ |key,value| !Configuration::OPTIONS.include?(key) }
+    if config.has_key?(lookup_name)
+      data.merge!(config[lookup_name])
+    end
+    data
+  end
 
-        # HTTP proxy server (user:pass@host:port)
-        [:http_proxy, nil],
+  class Configuration
+    include Singleton
 
-        # HTTPS proxy server (user:pass@host:port)
-        [:https_proxy, nil],
+    OPTIONS = [
+      :timeout,
+      :lookup,
+      :ip_lookup,
+      :language,
+      :http_headers,
+      :use_https,
+      :http_proxy,
+      :https_proxy,
+      :api_key,
+      :cache,
+      :cache_prefix,
+      :always_raise,
+      :units,
+      :distances
+    ]
 
-        # API key for geocoding service
-        # for Google Premier use a 3-element array: [key, client, channel]
-        [:api_key, nil],
+    attr_accessor :data
 
-        # cache object (must respond to #[], #[]=, and #keys)
-        [:cache, nil],
+    def self.set_defaults
+      instance.set_defaults
+    end
 
-        # prefix (string) to use for all cache keys
-        [:cache_prefix, "geocoder:"],
+    OPTIONS.each do |o|
+      define_method o do
+        @data[o]
+      end
+      define_method "#{o}=" do |value|
+        @data[o] = value
+      end
+    end
 
-        # exceptions that should not be rescued by default
-        # (if you want to implement custom error handling);
-        # supports SocketError and TimeoutError
-        [:always_raise, []]
-      ]
+    def configure(options)
+      @data.rmerge!(options)
     end
 
-    # define getters and setters for all configuration settings
-    self.options_and_defaults.each do |option, default|
-      class_eval(<<-END, __FILE__, __LINE__ + 1)
+    def initialize # :nodoc
+      @data = Geocoder::ConfigurationHash.new
+      set_defaults
+    end
 
-        @@#{option} = default unless defined? @@#{option}
+    def set_defaults
 
-        def self.#{option}
-          @@#{option}
-        end
+      # geocoding options
+      @data[:timeout]      = 3           # geocoding service timeout (secs)
+      @data[:lookup]       = :google     # name of street address geocoding service (symbol)
+      @data[:ip_lookup]    = :freegeoip  # name of IP address geocoding service (symbol)
+      @data[:language]     = :en         # ISO-639 language code
+      @data[:http_headers] = {}          # HTTP headers for lookup
+      @data[:use_https]    = false       # use HTTPS for lookup requests? (if supported)
+      @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
 
-        def self.#{option}=(obj)
-          @@#{option} = obj
-        end
+      # exceptions that should not be rescued by default
+      # (if you want to implement custom error handling);
+      # supports SocketError and TimeoutError
+      @data[:always_raise] = []
 
-      END
+      # calculation options
+      @data[:units]     = :mi      # :mi or :km
+      @data[:distances] = :linear  # :linear or :spherical
     end
+
+    instance_eval(OPTIONS.map do |option|
+      o = option.to_s
+      <<-EOS
+      def #{o}
+        instance.data[:#{o}]
+      end
+
+      def #{o}=(value)
+        instance.data[:#{o}] = value
+      end
+      EOS
+    end.join("\n\n"))
+
   end
 end
diff --git a/lib/geocoder/configuration_hash.rb b/lib/geocoder/configuration_hash.rb
new file mode 100644
index 0000000000000000000000000000000000000000..70569c321a39de41cd05a2907ed98874589e494a
--- /dev/null
+++ b/lib/geocoder/configuration_hash.rb
@@ -0,0 +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
+  end
+end
diff --git a/lib/geocoder/exceptions.rb b/lib/geocoder/exceptions.rb
index 208ca979e4bd91b338b774f3827be67cff3bfb9f..7445c662dc7aa548cc065c69452fa8f3573e0bd9 100644
--- a/lib/geocoder/exceptions.rb
+++ b/lib/geocoder/exceptions.rb
@@ -8,4 +8,14 @@ module Geocoder
 
   class OverQueryLimitError < Error
   end
+
+  class RequestDenied < Error
+  end
+
+  class InvalidRequest < Error
+  end
+
+  class InvalidApiKey < Error
+  end
+
 end
diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb
new file mode 100644
index 0000000000000000000000000000000000000000..472a164a01193e823eac1cea726dfae4f685f074
--- /dev/null
+++ b/lib/geocoder/lookup.rb
@@ -0,0 +1,81 @@
+module Geocoder
+  module Lookup
+    extend self
+
+    ##
+    # Array of valid Lookup service names.
+    #
+    def all_services
+      street_services + ip_services
+    end
+
+    ##
+    # Array of valid Lookup service names, excluding :test.
+    #
+    def all_services_except_test
+      all_services - [:test]
+    end
+
+    ##
+    # All street address lookup services, default first.
+    #
+    def street_services
+      [
+        :google,
+        :google_premier,
+        :yahoo,
+        :bing,
+        :geocoder_ca,
+        :yandex,
+        :nominatim,
+        :mapquest,
+        :test
+      ]
+    end
+
+    ##
+    # All IP address lookup services, default first.
+    #
+    def ip_services
+      [:freegeoip, :maxmind]
+    end
+
+    ##
+    # Retrieve a Lookup object from the store.
+    # Use this instead of Geocoder::Lookup::X.new to get an
+    # already-configured Lookup object.
+    #
+    def get(name)
+      @services = {} unless defined?(@services)
+      @services[name] = spawn(name) unless @services.include?(name)
+      @services[name]
+    end
+
+
+    private # -----------------------------------------------------------------
+
+    ##
+    # Spawn a Lookup of the given name.
+    #
+    def spawn(name)
+      if all_services.include?(name)
+        Geocoder::Lookup.const_get(classify_name(name)).new
+      else
+        valids = all_services.map(&:inspect).join(", ")
+        raise ConfigurationError, "Please specify a valid lookup for Geocoder " +
+          "(#{name.inspect} is not one of: #{valids})."
+      end
+    end
+
+    ##
+    # Convert an "underscore" version of a name into a "class" version.
+    #
+    def classify_name(filename)
+      filename.to_s.split("_").map{ |i| i[0...1].upcase + i[1..-1] }.join
+    end
+  end
+end
+
+Geocoder::Lookup.all_services.each do |name|
+  require "geocoder/lookups/#{name}"
+end
diff --git a/lib/geocoder/lookups/base.rb b/lib/geocoder/lookups/base.rb
index 838f633c527d76ebac3c9674785752167ec74a9b..4120fc7041646ec1790557987060d03101e8b02e 100644
--- a/lib/geocoder/lookups/base.rb
+++ b/lib/geocoder/lookups/base.rb
@@ -1,4 +1,5 @@
 require 'net/http'
+require 'net/https'
 require 'uri'
 
 unless defined?(ActiveSupport::JSON)
@@ -12,8 +13,24 @@ end
 
 module Geocoder
   module Lookup
+
     class Base
 
+      ##
+      # Human-readable name of the geocoding API.
+      #
+      def name
+        fail
+      end
+
+      ##
+      # Symbol which is used in configuration to refer to this Lookup.
+      #
+      def handle
+        str = self.class.to_s
+        str[str.rindex(':')+1..-1].gsub(/([a-z\d]+)([A-Z])/,'\1_\2').downcase.to_sym
+      end
+
       ##
       # Query the geocoding API and return a Geocoder::Result object.
       # Returns +nil+ on timeout or error.
@@ -22,18 +39,13 @@ module Geocoder
       # "205.128.54.202") for geocoding, or coordinates (latitude, longitude)
       # for reverse geocoding. Returns an array of <tt>Geocoder::Result</tt>s.
       #
-      def search(query)
-
-        # if coordinates given as string, turn into array
-        query = query.split(/\s*,\s*/) if coordinates?(query)
-
-        if query.is_a?(Array)
-          reverse = true
-          query = query.join(',')
-        else
-          reverse = false
-        end
-        results(query, reverse).map{ |r| result_class.new(r) }
+      def search(query, options = {})
+        query = Geocoder::Query.new(query, options) unless query.is_a?(Geocoder::Query)
+        results(query).map{ |r|
+          result = result_class.new(r)
+          result.cache_hit = @cache_hit if cache
+          result
+        }
       end
 
       ##
@@ -46,16 +58,37 @@ module Geocoder
         nil
       end
 
+      ##
+      # Array containing string descriptions of keys required by the API.
+      # Empty array if keys are optional or not required.
+      #
+      def required_api_key_parts
+        []
+      end
+
+      ##
+      # URL to use for querying the geocoding engine.
+      #
+      def query_url(query)
+        fail
+      end
 
       private # -------------------------------------------------------------
 
+      ##
+      # An object with configuration data for this particular lookup.
+      #
+      def configuration
+        Geocoder.config_for_lookup(handle)
+      end
+
       ##
       # Object used to make HTTP requests.
       #
       def http_client
-        protocol = "http#{'s' if Geocoder::Configuration.use_https}"
+        protocol = "http#{'s' if configuration.use_https}"
         proxy_name = "#{protocol}_proxy"
-        if proxy = Geocoder::Configuration.send(proxy_name)
+        if proxy = configuration.send(proxy_name)
           proxy_url = protocol + '://' + proxy
           begin
             uri = URI.parse(proxy_url)
@@ -72,15 +105,28 @@ module Geocoder
       ##
       # Geocoder::Result object or nil on timeout or other error.
       #
-      def results(query, reverse = false)
+      def results(query)
         fail
       end
 
+      def query_url_params(query)
+        query.options[:params] || {}
+      end
+
+      def url_query_string(query)
+        hash_to_query(
+          query_url_params(query).reject{ |key,value| value.nil? }
+        )
+      end
+
       ##
-      # URL to use for querying the geocoding engine.
+      # Key to use for caching a geocoding result. Usually this will be the
+      # request URL, but in cases where OAuth is used and the nonce,
+      # timestamp, etc varies from one request to another, we need to use
+      # something else (like the URL before OAuth encoding).
       #
-      def query_url(query, reverse = false)
-        fail
+      def cache_key(query)
+        query_url(query)
       end
 
       ##
@@ -95,7 +141,8 @@ module Geocoder
       # Return false if exception not raised.
       #
       def raise_error(error, message = nil)
-        if Geocoder::Configuration.always_raise.include?(error.class)
+        exceptions = configuration.always_raise
+        if exceptions == :all or exceptions.include?( error.is_a?(Class) ? error : error.class )
           raise error, message
         else
           false
@@ -105,30 +152,26 @@ module Geocoder
       ##
       # Returns a parsed search result (Ruby hash).
       #
-      def fetch_data(query, reverse = false)
-        begin
-          parse_raw_data fetch_raw_data(query, reverse)
-        rescue SocketError => err
-          raise_error(err) or warn "Geocoding API connection cannot be established."
-        rescue TimeoutError => err
-          raise_error(err) or warn "Geocoding API not responding fast enough " +
-            "(see Geocoder::Configuration.timeout to set limit)."
-        end
+      def fetch_data(query)
+        parse_raw_data fetch_raw_data(query)
+      rescue SocketError => err
+        raise_error(err) or warn "Geocoding API connection cannot be established."
+      rescue TimeoutError => err
+        raise_error(err) or warn "Geocoding API not responding fast enough " +
+          "(use Geocoder.configure(:timeout => ...) to set limit)."
       end
 
       ##
       # Parses a raw search result (returns hash or array).
       #
       def parse_raw_data(raw_data)
-        begin
-          if defined?(ActiveSupport::JSON)
-            ActiveSupport::JSON.decode(raw_data)
-          else
-            JSON.parse(raw_data)
-          end
-        rescue
-          warn "Geocoding API's response was not valid JSON."
+        if defined?(ActiveSupport::JSON)
+          ActiveSupport::JSON.decode(raw_data)
+        else
+          JSON.parse(raw_data)
         end
+      rescue
+        warn "Geocoding API's response was not valid JSON."
       end
 
       ##
@@ -136,48 +179,60 @@ module Geocoder
       # Set in configuration but not available for every service.
       #
       def protocol
-        "http" + (Geocoder::Configuration.use_https ? "s" : "")
+        "http" + (configuration.use_https ? "s" : "")
       end
 
       ##
-      # Fetches a raw search result (JSON string).
+      # Fetch a raw geocoding result (JSON string).
+      # The result might or might not be cached.
       #
-      def fetch_raw_data(query, reverse = false)
-        timeout(Geocoder::Configuration.timeout) do
-          url = query_url(query, reverse)
-          uri = URI.parse(url)
-          unless cache and body = cache[url]
-            client = http_client.new(uri.host, uri.port)
-            client.use_ssl = true if Geocoder::Configuration.use_https
-            response = client.get(uri.request_uri)
-            body = response.body
-            if cache and (200..399).include?(response.code.to_i)
-              cache[url] = body
-            end
+      def fetch_raw_data(query)
+        key = cache_key(query)
+        if cache and body = cache[key]
+          @cache_hit = true
+        else
+          check_api_key_configuration!(query)
+          response = make_api_request(query)
+          body = response.body
+          if cache and (200..399).include?(response.code.to_i)
+            cache[key] = body
           end
-          body
+          @cache_hit = false
         end
+        body
       end
 
       ##
-      # The working Cache object.
-      #
-      def cache
-        Geocoder.cache
+      # Make an HTTP(S) request to a geocoding API and
+      # return the response object.
+      #
+      def make_api_request(query)
+        timeout(configuration.timeout) do
+          uri = URI.parse(query_url(query))
+          client = http_client.new(uri.host, uri.port)
+          client.use_ssl = true if configuration.use_https
+          client.get(uri.request_uri, configuration.http_headers)
+        end
       end
 
-      ##
-      # Is the given string a loopback IP address?
-      #
-      def loopback_address?(ip)
-        !!(ip == "0.0.0.0" or ip.to_s.match(/^127/))
+      def check_api_key_configuration!(query)
+        key_parts = query.lookup.required_api_key_parts
+        if key_parts.size > Array(configuration.api_key).size
+          parts_string = key_parts.size == 1 ? key_parts.first : key_parts
+          raise Geocoder::ConfigurationError,
+            "The #{query.lookup.name} API requires a key to be configured: " +
+            parts_string.inspect
+        end
       end
 
       ##
-      # Does the given string look like latitude/longitude coordinates?
+      # The working Cache object.
       #
-      def coordinates?(value)
-        value.is_a?(String) and !!value.to_s.match(/^-?[0-9\.]+, *-?[0-9\.]+$/)
+      def cache
+        if @cache.nil? and store = configuration.cache
+          @cache = Cache.new(store, configuration.cache_prefix)
+        end
+        @cache
       end
 
       ##
diff --git a/lib/geocoder/lookups/bing.rb b/lib/geocoder/lookups/bing.rb
index 2bec5a4f1ec09a6443692b8a1fc356fe42dbbdfe..b99c8d6ce6e5a8d2df95b3f24f24bc6887827a04 100644
--- a/lib/geocoder/lookups/bing.rb
+++ b/lib/geocoder/lookups/bing.rb
@@ -4,30 +4,44 @@ require "geocoder/results/bing"
 module Geocoder::Lookup
   class Bing < Base
 
+    def name
+      "Bing"
+    end
+
     def map_link_url(coordinates)
       "http://www.bing.com/maps/default.aspx?cp=#{coordinates.join('~')}"
     end
 
+    def required_api_key_parts
+      ["key"]
+    end
+
+    def query_url(query)
+      "#{protocol}://dev.virtualearth.net/REST/v1/Locations" +
+        (query.reverse_geocode? ? "/#{query.sanitized_text}?" : "?") +
+        url_query_string(query)
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
-      return [] unless doc = fetch_data(query, reverse)
+    def results(query)
+      return [] unless doc = fetch_data(query)
 
-      if doc['statusDescription'] == "OK"
+      if doc['statusCode'] == 200
         return doc['resourceSets'].first['estimatedTotal'] > 0 ? doc['resourceSets'].first['resources'] : []
+      elsif doc['statusCode'] == 401 and doc["authenticationResultCode"] == "InvalidCredentials"
+        raise_error(Geocoder::InvalidApiKey) || warn("Invalid Bing API key.")
       else
         warn "Bing Geocoding API error: #{doc['statusCode']} (#{doc['statusDescription']})."
-        return []
       end
+      return []
     end
 
-    def query_url(query, reverse = false)
-      params = {:key => Geocoder::Configuration.api_key}
-      params[:query] = query unless reverse
-
-      base_url = "http://dev.virtualearth.net/REST/v1/Locations"
-      url_tail = reverse ? "/#{query}?" : "?"
-      base_url + url_tail + hash_to_query(params)
+    def query_url_params(query)
+      {
+        :key => configuration.api_key,
+        :query => query.reverse_geocode? ? nil : query.sanitized_text
+      }.merge(super)
     end
   end
 end
diff --git a/lib/geocoder/lookups/freegeoip.rb b/lib/geocoder/lookups/freegeoip.rb
index 63599a487cf768bf7d56033dcd108da17752793f..9efaaa4e377803e0b3c25a061debc6df4857c576 100644
--- a/lib/geocoder/lookups/freegeoip.rb
+++ b/lib/geocoder/lookups/freegeoip.rb
@@ -4,13 +4,25 @@ require 'geocoder/results/freegeoip'
 module Geocoder::Lookup
   class Freegeoip < Base
 
+    def name
+      "FreeGeoIP"
+    end
+
+    def query_url(query)
+      "#{protocol}://freegeoip.net/json/#{query.sanitized_text}"
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
+    def parse_raw_data(raw_data)
+      raw_data.match(/^<html><title>404/) ? nil : super(raw_data)
+    end
+
+    def results(query)
       # don't look up a loopback address, just return the stored result
-      return [reserved_result(query)] if loopback_address?(query)
+      return [reserved_result(query.text)] if query.loopback_ip_address?
       begin
-        return (doc = fetch_data(query, reverse)) ? [doc] : []
+        return (doc = fetch_data(query)) ? [doc] : []
       rescue StandardError => err # Freegeoip.net returns HTML on bad request
         raise_error(err)
         return []
@@ -31,9 +43,5 @@ module Geocoder::Lookup
         "country_code" => "RD"
       }
     end
-
-    def query_url(query, reverse = false)
-      "http://freegeoip.net/json/#{query}"
-    end
   end
 end
diff --git a/lib/geocoder/lookups/geocoder_ca.rb b/lib/geocoder/lookups/geocoder_ca.rb
index afaf147caad696e9501e2c80ca5d7a4a69820253..7ec7c1e97d32b59ac3bb58c005c3dabc70f23096 100644
--- a/lib/geocoder/lookups/geocoder_ca.rb
+++ b/lib/geocoder/lookups/geocoder_ca.rb
@@ -4,10 +4,18 @@ require "geocoder/results/geocoder_ca"
 module Geocoder::Lookup
   class GeocoderCa < Base
 
+    def name
+      "Geocoder.ca"
+    end
+
+    def query_url(query)
+      "#{protocol}://geocoder.ca/?" + url_query_string(query)
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
-      return [] unless doc = fetch_data(query, reverse)
+    def results(query)
+      return [] unless doc = fetch_data(query)
       if doc['error'].nil?
         return [doc]
       elsif doc['error']['code'] == "005"
@@ -18,23 +26,24 @@ module Geocoder::Lookup
       return []
     end
 
-    def query_url(query, reverse = false)
+    def query_url_params(query)
       params = {
         :geoit    => "xml",
         :jsonp    => 1,
         :callback => "test",
-        :auth     => Geocoder::Configuration.api_key
-      }
-      if reverse
-        lat,lon = query.split(',')
+        :auth     => configuration.api_key
+      }.merge(super)
+      if query.reverse_geocode?
+        lat,lon = query.coordinates
         params[:latt] = lat
         params[:longt] = lon
         params[:corner] = 1
         params[:reverse] = 1
       else
-        params[:locate] = query
+        params[:locate] = query.sanitized_text
+        params[:showpostal] = 1
       end
-      "http://geocoder.ca/?" + hash_to_query(params)
+      params
     end
 
     def parse_raw_data(raw_data)
diff --git a/lib/geocoder/lookups/google.rb b/lib/geocoder/lookups/google.rb
index 21ae3cfafb35412176bc4b4951bd58169bcfd0f6..c16bf85f8d23e913acdc10f7cb17a4cdc7e5172e 100644
--- a/lib/geocoder/lookups/google.rb
+++ b/lib/geocoder/lookups/google.rb
@@ -4,36 +4,59 @@ require "geocoder/results/google"
 module Geocoder::Lookup
   class Google < Base
 
+    def name
+      "Google"
+    end
+
     def map_link_url(coordinates)
       "http://maps.google.com/maps?q=#{coordinates.join(',')}"
     end
 
+    def query_url(query)
+      "#{protocol}://maps.googleapis.com/maps/api/geocode/json?" + url_query_string(query)
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
-      return [] unless doc = fetch_data(query, reverse)
+    def results(query)
+      return [] unless doc = fetch_data(query)
       case doc['status']; when "OK" # OK status implies >0 results
         return doc['results']
       when "OVER_QUERY_LIMIT"
         raise_error(Geocoder::OverQueryLimitError) ||
           warn("Google Geocoding API error: over query limit.")
       when "REQUEST_DENIED"
-        warn "Google Geocoding API error: request denied."
+        raise_error(Geocoder::RequestDenied) ||
+          warn("Google Geocoding API error: request denied.")
       when "INVALID_REQUEST"
-        warn "Google Geocoding API error: invalid request."
+        raise_error(Geocoder::InvalidRequest) ||
+          warn("Google Geocoding API error: invalid request.")
       end
       return []
     end
 
-    def query_url(query, reverse = false)
+    def query_url_google_params(query)
       params = {
-        (reverse ? :latlng : :address) => query,
+        (query.reverse_geocode? ? :latlng : :address) => query.sanitized_text,
         :sensor => "false",
-        :language => Geocoder::Configuration.language,
-        :key => Geocoder::Configuration.api_key
+        :language => configuration.language
       }
-      "#{protocol}://maps.googleapis.com/maps/api/geocode/json?" + hash_to_query(params)
+      unless (bounds = query.options[:bounds]).nil?
+        params[:bounds] = bounds.map{ |point| "%f,%f" % point }.join('|')
+      end
+      unless (region = query.options[:region]).nil?
+        params[:region] = region
+      end
+      unless (components = query.options[:components]).nil?
+        params[:components] = components.is_a?(Array) ? components.join("|") : components
+      end
+      params
+    end
+
+    def query_url_params(query)
+      query_url_google_params(query).merge(
+        :key => configuration.api_key
+      ).merge(super)
     end
   end
 end
-
diff --git a/lib/geocoder/lookups/google_premier.rb b/lib/geocoder/lookups/google_premier.rb
index 6befdddf585167868fcdbb58a042f0ea8175ea6d..d59adf9d1497007efef8edc2bd8a68ce7899e213 100644
--- a/lib/geocoder/lookups/google_premier.rb
+++ b/lib/geocoder/lookups/google_premier.rb
@@ -6,22 +6,31 @@ require 'geocoder/results/google_premier'
 module Geocoder::Lookup
   class GooglePremier < Google
 
-    private # ---------------------------------------------------------------
+    def name
+      "Google Premier"
+    end
+
+    def required_api_key_parts
+      ["private key", "client", "channel"]
+    end
 
-    def query_url(query, reverse = false)
-      params = {
-        (reverse ? :latlng : :address) => query,
-        :sensor => 'false',
-        :language => Geocoder::Configuration.language,
-        :client => Geocoder::Configuration.api_key[1],
-        :channel => Geocoder::Configuration.api_key[2]
-      }.reject{ |key, value| value.nil? }
-      path = "/maps/api/geocode/json?#{hash_to_query(params)}"
+    def query_url(query)
+      path = "/maps/api/geocode/json?" + url_query_string(query)
       "#{protocol}://maps.googleapis.com#{path}&signature=#{sign(path)}"
     end
 
+    private # ---------------------------------------------------------------
+
+    def query_url_params(query)
+      query_url_google_params(query).merge(super).merge(
+        :key => nil, # don't use param inherited from Google lookup
+        :client => configuration.api_key[1],
+        :channel => configuration.api_key[2]
+      )
+    end
+
     def sign(string)
-      raw_private_key = url_safe_base64_decode(Geocoder::Configuration.api_key[0])
+      raw_private_key = url_safe_base64_decode(configuration.api_key[0])
       digest = OpenSSL::Digest::Digest.new('sha1')
       raw_signature = OpenSSL::HMAC.digest(digest, raw_private_key, string)
       url_safe_base64_encode(raw_signature)
diff --git a/lib/geocoder/lookups/mapquest.rb b/lib/geocoder/lookups/mapquest.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ce0c8ac02f4dda4d8c30eaf4bb7efb1cdf786b6b
--- /dev/null
+++ b/lib/geocoder/lookups/mapquest.rb
@@ -0,0 +1,43 @@
+require 'cgi'
+require 'geocoder/lookups/base'
+require "geocoder/results/mapquest"
+
+module Geocoder::Lookup
+  class Mapquest < Base
+
+    def name
+      "Mapquest"
+    end
+
+    def required_api_key_parts
+      []
+    end
+
+    def query_url(query)
+      key = configuration.api_key
+      domain = key ? "www" : "open"
+      url = "#{protocol}://#{domain}.mapquestapi.com/geocoding/v1/#{search_type(query)}?"
+      url + url_query_string(query)
+    end
+
+    private # ---------------------------------------------------------------
+
+    def search_type(query)
+      query.reverse_geocode? ? "reverse" : "address"
+    end
+
+    def query_url_params(query)
+      params = { :location => query.sanitized_text }
+      if key = configuration.api_key
+        params[:key] = CGI.unescape(key)
+      end
+      params.merge(super)
+    end
+
+    def results(query)
+      return [] unless doc = fetch_data(query)
+      doc["results"][0]['locations']
+    end
+
+  end
+end
diff --git a/lib/geocoder/lookups/maxmind.rb b/lib/geocoder/lookups/maxmind.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e67fae8967a5630ee9ebc1bc81f64f6d502b140
--- /dev/null
+++ b/lib/geocoder/lookups/maxmind.rb
@@ -0,0 +1,88 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/maxmind'
+require 'csv'
+
+module Geocoder::Lookup
+  class Maxmind < Base
+
+    def name
+      "MaxMind"
+    end
+
+    def query_url(query)
+      "#{protocol}://geoip.maxmind.com/#{service_code}?" + url_query_string(query)
+    end
+
+    private # ---------------------------------------------------------------
+
+    ##
+    # Return the name of the configured service, or raise an exception.
+    #
+    def configured_service!
+      if s = configuration[:service] and services.keys.include?(s)
+        return s
+      else
+        raise(
+          Geocoder::ConfigurationError,
+          "When using MaxMind you MUST specify a service name: " +
+          "Geocoder.configure(:maxmind => {:service => ...}), " +
+          "where '...' is one of: #{services.keys.inspect}"
+        )
+      end
+    end
+
+    def service_code
+      services[configured_service!]
+    end
+
+    def service_response_fields_count
+      Geocoder::Result::Maxmind.field_names[configured_service!].size
+    end
+
+    def data_contains_error?(parsed_data)
+      # if all fields given then there is an error
+      parsed_data.size == service_response_fields_count and !parsed_data.last.nil?
+    end
+
+    ##
+    # Service names mapped to code used in URL.
+    #
+    def services
+      {
+        :country => "a",
+        :city => "b",
+        :city_isp_org => "f",
+        :omni => "e"
+      }
+    end
+
+    def results(query)
+      # don't look up a loopback address, just return the stored result
+      return [reserved_result] if query.loopback_ip_address?
+      doc = fetch_data(query)
+      if doc and doc.is_a?(Array)
+        if !data_contains_error?(doc)
+          return [doc]
+        elsif doc.last == "INVALID_LICENSE_KEY"
+          raise_error(Geocoder::InvalidApiKey) || warn("Invalid MaxMind API key.")
+        end
+      end
+      return []
+    end
+
+    def parse_raw_data(raw_data)
+      CSV.parse_line raw_data
+    end
+
+    def reserved_result
+      ",,,,0,0,0,0,,,"
+    end
+
+    def query_url_params(query)
+      {
+        :l => configuration.api_key,
+        :i => query.sanitized_text
+      }.merge(super)
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/nominatim.rb b/lib/geocoder/lookups/nominatim.rb
index 12f2c1513879cb87346341c96b2aa92f30cab9bf..e8d36d00c17b6db7e51a1477b7dbf0358feb0b48 100644
--- a/lib/geocoder/lookups/nominatim.rb
+++ b/lib/geocoder/lookups/nominatim.rb
@@ -4,34 +4,42 @@ require "geocoder/results/nominatim"
 module Geocoder::Lookup
   class Nominatim < Base
 
+    def name
+      "Nominatim"
+    end
+
     def map_link_url(coordinates)
       "http://www.openstreetmap.org/?lat=#{coordinates[0]}&lon=#{coordinates[1]}&zoom=15&layers=M"
     end
 
+    def query_url(query)
+      method = query.reverse_geocode? ? "reverse" : "search"
+      host = configuration[:host] || "nominatim.openstreetmap.org"
+      "#{protocol}://#{host}/#{method}?" + url_query_string(query)
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
-      return [] unless doc = fetch_data(query, reverse)
+    def results(query)
+      return [] unless doc = fetch_data(query)
       doc.is_a?(Array) ? doc : [doc]
     end
 
-    def query_url(query, reverse = false)
+    def query_url_params(query)
       params = {
         :format => "json",
         :polygon => "1",
         :addressdetails => "1",
-        :"accept-language" => Geocoder::Configuration.language
-      }
-      if (reverse)
-        method = 'reverse'
-        parts = query.split(/\s*,\s*/);
-        params[:lat] = parts[0]
-        params[:lon] = parts[1]
+        :"accept-language" => configuration.language
+      }.merge(super)
+      if query.reverse_geocode?
+        lat,lon = query.coordinates
+        params[:lat] = lat
+        params[:lon] = lon
       else
-        method = 'search'
-        params[:q] = query
+        params[:q] = query.sanitized_text
       end
-      "http://nominatim.openstreetmap.org/#{method}?" + hash_to_query(params)
+      params
     end
   end
 end
diff --git a/lib/geocoder/lookups/test.rb b/lib/geocoder/lookups/test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ebf7b5d76a2d201aeb39599826d0b1100ce22fd0
--- /dev/null
+++ b/lib/geocoder/lookups/test.rb
@@ -0,0 +1,38 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/test'
+
+module Geocoder
+  module Lookup
+    class Test < Base
+
+      def name
+        "Test"
+      end
+
+      def self.add_stub(query_text, results)
+        stubs[query_text] = results
+      end
+
+      def self.read_stub(query_text)
+        stubs.fetch(query_text) {
+          raise ArgumentError, "unknown stub request #{query_text}"
+        }
+      end
+
+      def self.stubs
+        @stubs ||= {}
+      end
+
+      def self.reset
+        @stubs = {}
+      end
+
+      private
+
+      def results(query)
+        Geocoder::Lookup::Test.read_stub(query.text)
+      end
+
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/yahoo.rb b/lib/geocoder/lookups/yahoo.rb
index 1ce523ecc304a7973f5062e275cfc78473972a43..0c27cc0c77cde05e30c85c5a1adfae9852caa9ed 100644
--- a/lib/geocoder/lookups/yahoo.rb
+++ b/lib/geocoder/lookups/yahoo.rb
@@ -1,34 +1,84 @@
 require 'geocoder/lookups/base'
 require "geocoder/results/yahoo"
+require 'oauth_util'
 
 module Geocoder::Lookup
   class Yahoo < Base
 
+    def name
+      "Yahoo BOSS"
+    end
+
     def map_link_url(coordinates)
       "http://maps.yahoo.com/#lat=#{coordinates[0]}&lon=#{coordinates[1]}"
     end
 
+    def required_api_key_parts
+      ["consumer key", "consumer secret"]
+    end
+
+    def query_url(query)
+      parsed_url = URI.parse(raw_url(query))
+      o = OauthUtil.new
+      o.consumer_key = configuration.api_key[0]
+      o.consumer_secret = configuration.api_key[1]
+      base_url + o.sign(parsed_url).query_string
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
-      return [] unless doc = fetch_data(query, reverse)
-      if doc = doc['ResultSet'] and doc['Error'] == 0
-        return doc['Found'] > 0 ? doc['Results'] : []
+    def results(query)
+      return [] unless doc = fetch_data(query)
+      doc = doc['bossresponse']
+      if doc['responsecode'].to_i == 200
+        if doc['placefinder']['count'].to_i > 0
+          return doc['placefinder']['results']
+        else
+          return []
+        end
       else
-        warn "Yahoo Geocoding API error: #{doc['Error']} (#{doc['ErrorMessage']})."
+        warn "Yahoo Geocoding API error: #{doc['responsecode']} (#{doc['reason']})."
         return []
       end
     end
 
-    def query_url(query, reverse = false)
-      params = {
-        :location => query,
+    ##
+    # Yahoo returns errors as XML even when JSON format is specified.
+    # Handle that here, without parsing the XML
+    # (which would add unnecessary complexity).
+    #
+    def parse_raw_data(raw_data)
+      if raw_data.match /^<\?xml/
+        if raw_data.include?("Rate Limit Exceeded")
+          raise_error(Geocoder::OverQueryLimitError) || warn("Over API query limit.")
+        elsif raw_data.include?("Please provide valid credentials")
+          raise_error(Geocoder::InvalidApiKey) || warn("Invalid API key.")
+        end
+      else
+        super(raw_data)
+      end
+    end
+
+    def query_url_params(query)
+      {
+        :location => query.sanitized_text,
         :flags => "JXTSR",
-        :gflags => "AC#{'R' if reverse}",
-        :locale => "#{Geocoder::Configuration.language}_US",
-        :appid => Geocoder::Configuration.api_key
-      }
-      "http://where.yahooapis.com/geocode?" + hash_to_query(params)
+        :gflags => "AC#{'R' if query.reverse_geocode?}",
+        :locale => "#{configuration.language}_US",
+        :appid => configuration.api_key
+      }.merge(super)
+    end
+
+    def cache_key(query)
+      raw_url(query)
+    end
+
+    def base_url
+      "#{protocol}://yboss.yahooapis.com/geo/placefinder?"
+    end
+
+    def raw_url(query)
+      base_url + url_query_string(query)
     end
   end
 end
diff --git a/lib/geocoder/lookups/yandex.rb b/lib/geocoder/lookups/yandex.rb
index 452d27694eea2728450e6cb05877b909ee441f7f..f5559efbdd865ce4080358acd1b303595f1b1199 100644
--- a/lib/geocoder/lookups/yandex.rb
+++ b/lib/geocoder/lookups/yandex.rb
@@ -4,16 +4,28 @@ require "geocoder/results/yandex"
 module Geocoder::Lookup
   class Yandex < Base
 
+    def name
+      "Yandex"
+    end
+
     def map_link_url(coordinates)
       "http://maps.yandex.ru/?ll=#{coordinates.reverse.join(',')}"
     end
 
+    def query_url(query)
+      "#{protocol}://geocode-maps.yandex.ru/1.x/?" + url_query_string(query)
+    end
+
     private # ---------------------------------------------------------------
 
-    def results(query, reverse = false)
-      return [] unless doc = fetch_data(query, reverse)
+    def results(query)
+      return [] unless doc = fetch_data(query)
       if err = doc['error']
-        warn "Yandex Geocoding API error: #{err['status']} (#{err['message']})."
+        if err["status"] == 401 and err["message"] == "invalid key"
+          raise_error(Geocoder::InvalidApiKey) || warn("Invalid API key.")
+        else
+          warn "Yandex Geocoding API error: #{err['status']} (#{err['message']})."
+        end
         return []
       end
       if doc = doc['response']['GeoObjectCollection']
@@ -25,15 +37,18 @@ module Geocoder::Lookup
       end
     end
 
-    def query_url(query, reverse = false)
-      query = query.split(",").reverse.join(",") if reverse
-      params = {
-        :geocode => query,
+    def query_url_params(query)
+      if query.reverse_geocode?
+        q = query.coordinates.reverse.join(",")
+      else
+        q = query.sanitized_text
+      end
+      {
+        :geocode => q,
         :format => "json",
-        :plng => "#{Geocoder::Configuration.language}", # supports ru, uk, be
-        :key => Geocoder::Configuration.api_key
-      }
-      "http://geocode-maps.yandex.ru/1.x/?" + hash_to_query(params)
+        :plng => "#{configuration.language}", # supports ru, uk, be
+        :key => configuration.api_key
+      }.merge(super)
     end
   end
 end
diff --git a/lib/geocoder/models/active_record.rb b/lib/geocoder/models/active_record.rb
index 67a17106accb7e66632091aac450bed5ceb2f699..29bbe5f406d5534ffc674599c28d4c86340f10ce 100644
--- a/lib/geocoder/models/active_record.rb
+++ b/lib/geocoder/models/active_record.rb
@@ -14,7 +14,9 @@ module Geocoder
           :user_address  => address_attr,
           :latitude      => options[:latitude]  || :latitude,
           :longitude     => options[:longitude] || :longitude,
-          :geocode_block => block
+          :geocode_block => block,
+          :units         => options[:units],
+          :method        => options[:method]
         )
       end
 
@@ -27,7 +29,9 @@ module Geocoder
           :fetched_address => options[:address] || :address,
           :latitude        => latitude_attr,
           :longitude       => longitude_attr,
-          :reverse_block   => block
+          :reverse_block   => block,
+          :units         => options[:units],
+          :method        => options[:method]
         )
       end
 
@@ -39,3 +43,4 @@ module Geocoder
     end
   end
 end
+
diff --git a/lib/geocoder/models/base.rb b/lib/geocoder/models/base.rb
index 836233162770542dc063784b4d448c8324792708..34853a5b7af77a6bb1b692d2d053858b7e7784d1 100644
--- a/lib/geocoder/models/base.rb
+++ b/lib/geocoder/models/base.rb
@@ -12,7 +12,9 @@ module Geocoder
         if defined?(@geocoder_options)
           @geocoder_options
         elsif superclass.respond_to?(:geocoder_options)
-          superclass.geocoder_options
+          superclass.geocoder_options || { }
+        else
+          { }
         end
       end
 
@@ -24,7 +26,6 @@ module Geocoder
         fail
       end
 
-
       private # ----------------------------------------------------------------
 
       def geocoder_init(options)
@@ -38,3 +39,4 @@ module Geocoder
     end
   end
 end
+
diff --git a/lib/geocoder/models/mongo_base.rb b/lib/geocoder/models/mongo_base.rb
index 15bbbabe848b51c99247c2339dae34b764f45b1a..fe7a0e1f5fe968b1178aaed8b655b2087816cc42 100644
--- a/lib/geocoder/models/mongo_base.rb
+++ b/lib/geocoder/models/mongo_base.rb
@@ -16,7 +16,10 @@ module Geocoder
           :geocode       => true,
           :user_address  => address_attr,
           :coordinates   => options[:coordinates] || :coordinates,
-          :geocode_block => block
+          :geocode_block => block,
+          :units         => options[:units],
+          :method        => options[:method],
+          :skip_index    => options[:skip_index] || false
         )
       end
 
@@ -28,7 +31,10 @@ module Geocoder
           :reverse_geocode => true,
           :fetched_address => options[:address] || :address,
           :coordinates     => coordinates_attr,
-          :reverse_block   => block
+          :reverse_block   => block,
+          :units           => options[:units],
+          :method          => options[:method],
+          :skip_index      => options[:skip_index] || false
         )
       end
 
@@ -36,7 +42,7 @@ module Geocoder
 
       def geocoder_init(options)
         unless geocoder_initialized?
-          @geocoder_options = {}
+          @geocoder_options = { }
           require "geocoder/stores/#{geocoder_file_name}"
           include Geocoder::Store.const_get(geocoder_module_name)
         end
@@ -44,12 +50,11 @@ module Geocoder
       end
 
       def geocoder_initialized?
-        begin
-          included_modules.include? Geocoder::Store.const_get(geocoder_module_name)
-        rescue NameError
-          false
-        end
+        included_modules.include? Geocoder::Store.const_get(geocoder_module_name)
+      rescue NameError
+        false
       end
     end
   end
 end
+
diff --git a/lib/geocoder/models/mongo_mapper.rb b/lib/geocoder/models/mongo_mapper.rb
index a093f6bf056a1418ce422c38d7c80994ae3a1011..17d83402e71e78fe65a4e97386d0582e163509fb 100644
--- a/lib/geocoder/models/mongo_mapper.rb
+++ b/lib/geocoder/models/mongo_mapper.rb
@@ -16,8 +16,10 @@ module Geocoder
 
       def geocoder_init(options)
         super(options)
-        ensure_index [[ geocoder_options[:coordinates], Mongo::GEO2D ]],
-          :min => -180, :max => 180 # create 2d index
+        if options[:skip_index] == false
+          ensure_index [[ geocoder_options[:coordinates], Mongo::GEO2D ]],
+            :min => -180, :max => 180 # create 2d index
+        end
       end
     end
   end
diff --git a/lib/geocoder/models/mongoid.rb b/lib/geocoder/models/mongoid.rb
index e3ca53a3254c38ca9145f1db221727a2727c5486..b1497327f32f583839502e73fca90ba7b7f75bc2 100644
--- a/lib/geocoder/models/mongoid.rb
+++ b/lib/geocoder/models/mongoid.rb
@@ -16,8 +16,16 @@ module Geocoder
 
       def geocoder_init(options)
         super(options)
-        index [[ geocoder_options[:coordinates], Mongo::GEO2D ]],
-          :min => -180, :max => 180 # create 2d index
+        if options[:skip_index] == false
+          # create 2d index
+          if defined?(::Mongoid::VERSION) && ::Mongoid::VERSION >= "3"
+            index({ geocoder_options[:coordinates].to_sym => '2d' }, 
+                  {:min => -180, :max => 180})
+          else
+            index [[ geocoder_options[:coordinates], '2d' ]],
+              :min => -180, :max => 180
+          end
+        end
       end
     end
   end
diff --git a/lib/geocoder/query.rb b/lib/geocoder/query.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c17ba159f05a96aeae0675bf9329b034acddb955
--- /dev/null
+++ b/lib/geocoder/query.rb
@@ -0,0 +1,103 @@
+module Geocoder
+  class Query
+    attr_accessor :text, :options
+
+    def initialize(text, options = {})
+      self.text = text
+      self.options = options
+    end
+
+    def execute
+      lookup.search(text, options)
+    end
+
+    def to_s
+      text
+    end
+
+    def sanitized_text
+      if coordinates?
+        text.split(/\s*,\s*/).join(',')
+      else
+        text
+      end
+    end
+
+    ##
+    # Get a Lookup object (which communicates with the remote geocoding API)
+    # appropriate to the Query text.
+    #
+    def lookup
+      if ip_address?
+        name = Configuration.ip_lookup || Geocoder::Lookup.ip_services.first
+      else
+        name = Configuration.lookup || Geocoder::Lookup.street_services.first
+      end
+      Lookup.get(name)
+    end
+
+    def url
+      lookup.query_url(self)
+    end
+
+    ##
+    # Is the Query blank? (ie, should we not bother searching?)
+    # A query is considered blank if its text is nil or empty string AND
+    # no URL parameters are specified.
+    #
+    def blank?
+      !params_given? and (
+        (text.is_a?(Array) and text.compact.size < 2) or
+        text.to_s.match(/^\s*$/)
+      )
+    end
+
+    ##
+    # Does the Query text look like an IP address?
+    #
+    # Does not check for actual validity, just the appearance of four
+    # dot-delimited numbers.
+    #
+    def ip_address?
+      !!text.to_s.match(/^(::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
+    end
+
+    ##
+    # Is the Query text a loopback IP address?
+    #
+    def loopback_ip_address?
+      !!(text == "0.0.0.0" or text.to_s.match(/^127/))
+    end
+
+    ##
+    # Does the given string look like latitude/longitude coordinates?
+    #
+    def coordinates?
+      text.is_a?(Array) or (
+        text.is_a?(String) and
+        !!text.to_s.match(/^-?[0-9\.]+, *-?[0-9\.]+$/)
+      )
+    end
+
+    ##
+    # Return the latitude/longitude coordinates specified in the query,
+    # or nil if none.
+    #
+    def coordinates
+      sanitized_text.split(',') if coordinates?
+    end
+
+    ##
+    # Should reverse geocoding be performed for this query?
+    #
+    def reverse_geocode?
+      coordinates?
+    end
+
+    private # ----------------------------------------------------------------
+
+    def params_given?
+      !!(options[:params].is_a?(Hash) and options[:params].keys.size > 0)
+    end
+  end
+end
diff --git a/lib/geocoder/request.rb b/lib/geocoder/request.rb
index 5f71d46bc86455f9be4b2771b545a16bf5384653..f02aab4770dea1c1a563fc653778b130aaaa0bb5 100644
--- a/lib/geocoder/request.rb
+++ b/lib/geocoder/request.rb
@@ -5,7 +5,13 @@ module Geocoder
 
     def location
       unless defined?(@location)
-        @location = Geocoder.search(ip).first
+        if env.has_key?('HTTP_X_REAL_IP')
+          @location = Geocoder.search(env['HTTP_X_REAL_IP']).first
+        elsif env.has_key?('HTTP_X_FORWARDED_FOR')
+          @location = Geocoder.search(env['HTTP_X_FORWARDED_FOR'].split(/\s*,\s*/)[0]).first
+        else
+          @location = Geocoder.search(ip).first
+        end
       end
       @location
     end
diff --git a/lib/geocoder/results/base.rb b/lib/geocoder/results/base.rb
index 645e3c2a29e1400f4e25b567aaafed4eba3040de..a798ced6852dea49b1eaa8f23fe75298bd07021d 100644
--- a/lib/geocoder/results/base.rb
+++ b/lib/geocoder/results/base.rb
@@ -1,13 +1,20 @@
 module Geocoder
   module Result
     class Base
+
+      # data (hash) fetched from geocoding service
       attr_accessor :data
 
+      # true if result came from cache, false if from request to geocoding
+      # service; nil if cache is not configured
+      attr_accessor :cache_hit
+
       ##
-      # Takes a hash of result data from a parsed Google result document.
+      # Takes a hash of data from a parsed geocoding service response.
       #
       def initialize(data)
         @data = data
+        @cache_hit = nil
       end
 
       ##
diff --git a/lib/geocoder/results/google.rb b/lib/geocoder/results/google.rb
index 73e2289db58c0027b006f28d4520107e753e10b1..d692b8e8aba2c1d23785b693bf5b90c268a95dd3 100644
--- a/lib/geocoder/results/google.rb
+++ b/lib/geocoder/results/google.rb
@@ -53,6 +53,22 @@ module Geocoder::Result
       end
     end
 
+    def route
+      if route = address_components_of_type(:route).first
+        route['long_name']
+      end
+    end
+
+    def street_number
+      if street_number = address_components_of_type(:street_number).first
+        street_number['long_name']
+      end
+    end
+
+    def street_address
+      [street_number, route].compact.join(' ')
+    end
+
     def types
       @data['types']
     end
diff --git a/lib/geocoder/results/mapquest.rb b/lib/geocoder/results/mapquest.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bb4bdc461a318c3d97a30b3a093b10a2cf8bffef
--- /dev/null
+++ b/lib/geocoder/results/mapquest.rb
@@ -0,0 +1,51 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Mapquest < Base
+    def latitude
+      @data["latLng"]["lat"]
+    end
+
+    def longitude
+      @data["latLng"]["lng"]
+    end
+
+    def coordinates
+      [latitude, longitude]
+    end
+
+    def city
+      @data['adminArea5']
+    end
+
+    def street
+      @data['street']
+    end
+
+    def state
+      @data['adminArea3']
+    end
+
+    alias_method :state_code, :state
+
+    #FIXME: these might not be right, unclear with MQ documentation
+    alias_method :province, :state
+    alias_method :province_code, :state
+
+    def postal_code
+      @data['postalCode'].to_s
+    end
+
+    def country
+      @data['adminArea1']
+    end
+
+    def country_code
+      country
+    end
+
+    def address
+      [street, city, state, postal_code, country].reject{|s| s.length == 0 }.join(", ")
+    end
+  end
+end
diff --git a/lib/geocoder/results/maxmind.rb b/lib/geocoder/results/maxmind.rb
new file mode 100644
index 0000000000000000000000000000000000000000..aa6c7aa92b2cafaa63c83aff6f6f6abec7f7d421
--- /dev/null
+++ b/lib/geocoder/results/maxmind.rb
@@ -0,0 +1,136 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Maxmind < Base
+
+    ##
+    # Hash mapping service names to names of returned fields.
+    #
+    def self.field_names
+      {
+        :country => [
+          :country_code,
+          :error
+        ],
+
+        :city => [
+          :country_code,
+          :region_code,
+          :city_name,
+          :latitude,
+          :longitude,
+          :error
+        ],
+
+        :city_isp_org => [
+          :country_code,
+          :region_code,
+          :city_name,
+          :postal_code,
+          :latitude,
+          :longitude,
+          :metro_code,
+          :area_code,
+          :isp_name,
+          :organization_name,
+          :error
+        ],
+
+        :omni => [
+          :country_code,
+          :country_name,
+          :region_code,
+          :region_name,
+          :city_name,
+          :latitude,
+          :longitude,
+          :metro_code,
+          :area_code,
+          :time_zone,
+          :continent_code,
+          :postal_code,
+          :isp_name,
+          :organization_name,
+          :domain,
+          :as_number,
+          :netspeed,
+          :user_type,
+          :accuracy_radius,
+          :country_confidence_factor,
+          :city_confidence_factor,
+          :region_confidence_factor,
+          :postal_confidence_factor,
+          :error
+        ]
+      }
+    end
+
+    ##
+    # Name of the MaxMind service being used.
+    # Inferred from format of @data.
+    #
+    def service_name
+      self.class.field_names.to_a.each do |n,f|
+        return n if (@data.size..@data.size+1).include?(f.size)
+      end
+      nil
+    end
+
+    def field_names
+      self.class.field_names[service_name]
+    end
+
+    def data_hash
+      @data_hash ||= Hash[*field_names.zip(@data).flatten]
+    end
+
+    def coordinates
+      [data_hash[:latitude].to_f, data_hash[:longitude].to_f]
+    end
+
+    def address(format = :full)
+      s = state_code.to_s == "" ? "" : ", #{state_code}"
+      "#{city}#{s} #{postal_code}, #{country_code}".sub(/^[ ,]*/, "")
+    end
+
+    def city
+      data_hash[:city_name]
+    end
+
+    def state # not given by MaxMind
+      data_hash[:region_name] || data_hash[:region_code]
+    end
+
+    def state_code
+      data_hash[:region_code]
+    end
+
+    def country #not given by MaxMind
+      data_hash[:country_name] || data_hash[:country_code]
+    end
+
+    def country_code
+      data_hash[:country_code]
+    end
+
+    def postal_code
+      data_hash[:postal_code]
+    end
+
+    def method_missing(method, *args, &block)
+      if field_names.include?(method)
+        data_hash[method]
+      else
+        super
+      end
+    end
+
+    def respond_to?(method)
+      if field_names.include?(method)
+        true
+      else
+        super
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/nominatim.rb b/lib/geocoder/results/nominatim.rb
index cc9dce3ab56d8bc53ea0d97058996febb3fbfc74..249d75f520cf50787668d544b3a715e53f2658ab 100644
--- a/lib/geocoder/results/nominatim.rb
+++ b/lib/geocoder/results/nominatim.rb
@@ -3,6 +3,13 @@ require 'geocoder/results/base'
 module Geocoder::Result
   class Nominatim < Base
 
+    def poi
+      %w[stadium bus_stop tram_stop].each do |key|
+        return @data['address'][key] if @data['address'].key?(key)
+      end
+      return nil
+    end
+
     def house_number
       @data['address']['house_number']
     end
@@ -12,15 +19,21 @@ module Geocoder::Result
     end
 
     def street
-      @data['address']['road']
+      %w[road pedestrian highway].each do |key|
+        return @data['address'][key] if @data['address'].key?(key)
+      end
+      return nil
     end
 
     def city
-      @data['address']['city']
+      %w[city town village hamlet].each do |key|
+        return @data['address'][key] if @data['address'].key?(key)
+      end
+      return nil
     end
 
     def village
-      @data['address']['villiage']
+      @data['address']['village']
     end
 
     def town
@@ -49,18 +62,32 @@ module Geocoder::Result
       @data['address']['country_code']
     end
 
+    def suburb
+      @data['address']['suburb']
+    end
+
     def coordinates
       [@data['lat'].to_f, @data['lon'].to_f]
     end
 
+    def place_class
+      @data['class']
+    end
+
+    def place_type
+      @data['type']
+    end
+
     def self.response_attributes
-      %w[place_id, osm_type, osm_id, boundingbox, license,
-         polygonpoints, display_name, class, type, stadium, suburb]
+      %w[place_id osm_type osm_id boundingbox license
+         polygonpoints display_name class type stadium]
     end
 
     response_attributes.each do |a|
-      define_method a do
-        @data[a]
+      unless method_defined?(a)
+        define_method a do
+          @data[a]
+        end
       end
     end
   end
diff --git a/lib/geocoder/results/test.rb b/lib/geocoder/results/test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b735952173c58d73541c97aec893aacbf2f46ff8
--- /dev/null
+++ b/lib/geocoder/results/test.rb
@@ -0,0 +1,16 @@
+require 'geocoder/results/base'
+
+module Geocoder
+  module Result
+    class Test < Base
+
+      %w[latitude longitude city state state_code province
+      province_code postal_code country country_code address
+      street_address street_number route].each do |attr|
+        define_method(attr) do
+          @data[attr.to_s] || @data[attr.to_sym]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/yahoo.rb b/lib/geocoder/results/yahoo.rb
index fab775c545deb39f0826d1223d45d5310802ccef..cb0fed3369a02f71d081c103685f41ab7d01ccbc 100644
--- a/lib/geocoder/results/yahoo.rb
+++ b/lib/geocoder/results/yahoo.rb
@@ -31,17 +31,24 @@ module Geocoder::Result
       @data['postal']
     end
 
+    def address_hash
+      @data['hash']
+    end
+
     def self.response_attributes
       %w[quality offsetlat offsetlon radius boundingbox name
         line1 line2 line3 line4 cross house street xstreet unittype unit
+        city state statecode country countrycode postal
         neighborhood county countycode
         level0 level1 level2 level3 level4 level0code level1code level2code
         timezone areacode uzip hash woeid woetype]
     end
 
     response_attributes.each do |a|
-      define_method a do
-        @data[a]
+      unless method_defined?(a)
+        define_method a do
+          @data[a]
+        end
       end
     end
   end
diff --git a/lib/geocoder/results/yandex.rb b/lib/geocoder/results/yandex.rb
index f69c10cfc89810471ff3c41d8bdd1d43d2f97866..b1b0952a6579bac8cb9ab56b798a8a7ed572b8fc 100644
--- a/lib/geocoder/results/yandex.rb
+++ b/lib/geocoder/results/yandex.rb
@@ -59,6 +59,10 @@ module Geocoder::Result
       address_details['Locality']['Premise']['PremiseName']
     end
 
+    def precision
+      @data['GeoObject']['metaDataProperty']['GeocoderMetaData']['precision']
+    end
+
     private # ----------------------------------------------------------------
 
     def address_details
diff --git a/lib/geocoder/sql.rb b/lib/geocoder/sql.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8d390b4391180643cbdc1f533fbc4c191049f079
--- /dev/null
+++ b/lib/geocoder/sql.rb
@@ -0,0 +1,106 @@
+module Geocoder
+  module Sql
+    extend self
+
+    ##
+    # Distance calculation for use with a database that supports POWER(),
+    # SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
+    # ATAN2(), DEGREES(), and RADIANS().
+    #
+    # Based on the excellent tutorial at:
+    # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
+    #
+    def full_distance(latitude, longitude, lat_attr, lon_attr, options = {})
+      units = options[:units] || Geocoder.config.units
+      earth = Geocoder::Calculations.earth_radius(units)
+
+      "#{earth} * 2 * ASIN(SQRT(" +
+        "POWER(SIN((#{latitude.to_f} - #{lat_attr}) * PI() / 180 / 2), 2) + " +
+        "COS(#{latitude.to_f} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " +
+        "POWER(SIN((#{longitude.to_f} - #{lon_attr}) * PI() / 180 / 2), 2)" +
+      "))"
+    end
+
+    ##
+    # Distance calculation for use with a database without trigonometric
+    # functions, like SQLite. Approach is to find objects within a square
+    # rather than a circle, so results are very approximate (will include
+    # objects outside the given radius).
+    #
+    # Distance and bearing calculations are *extremely inaccurate*. To be
+    # clear: this only exists to provide interface consistency. Results
+    # are not intended for use in production!
+    #
+    def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {})
+      units = options[:units] || Geocoder.config.units
+      dx = Geocoder::Calculations.longitude_degree_distance(30, units)
+      dy = Geocoder::Calculations.latitude_degree_distance(units)
+
+      # sin of 45 degrees = average x or y component of vector
+      factor = Math.sin(Math::PI / 4)
+
+      "(#{dy} * ABS(#{lat_attr} - #{latitude.to_f}) * #{factor}) + " +
+        "(#{dx} * ABS(#{lon_attr} - #{longitude.to_f}) * #{factor})"
+    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 "
+      # 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}"
+      else
+        spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
+      end
+    end
+
+    ##
+    # Fairly accurate bearing calculation. Takes a latitude, longitude,
+    # and an options hash which must include a :bearing value
+    # (:linear or :spherical).
+    #
+    # Based on:
+    # http://www.beginningspatial.com/calculating_bearing_one_point_another
+    #
+    def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
+      case options[:bearing] || Geocoder.config.distances
+      when :linear
+        "CAST(" +
+          "DEGREES(ATAN2( " +
+            "RADIANS(#{lon_attr} - #{longitude.to_f}), " +
+            "RADIANS(#{lat_attr} - #{latitude.to_f})" +
+          ")) + 360 " +
+        "AS decimal) % 360"
+      when :spherical
+        "CAST(" +
+          "DEGREES(ATAN2( " +
+            "SIN(RADIANS(#{lon_attr} - #{longitude.to_f})) * " +
+            "COS(RADIANS(#{lat_attr})), (" +
+              "COS(RADIANS(#{latitude.to_f})) * SIN(RADIANS(#{lat_attr}))" +
+            ") - (" +
+              "SIN(RADIANS(#{latitude.to_f})) * COS(RADIANS(#{lat_attr})) * " +
+              "COS(RADIANS(#{lon_attr} - #{longitude.to_f}))" +
+            ")" +
+          ")) + 360 " +
+        "AS decimal) % 360"
+      end
+    end
+
+    ##
+    # Totally lame bearing calculation. Basically useless except that it
+    # returns *something* in databases without trig functions.
+    #
+    def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
+      "CASE " +
+        "WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
+          "#{lon_attr} >= #{longitude.to_f}) THEN  45.0 " +
+        "WHEN (#{lat_attr} <  #{latitude.to_f} AND " +
+          "#{lon_attr} >= #{longitude.to_f}) THEN 135.0 " +
+        "WHEN (#{lat_attr} <  #{latitude.to_f} AND " +
+          "#{lon_attr} <  #{longitude.to_f}) THEN 225.0 " +
+        "WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
+          "#{lon_attr} <  #{longitude.to_f}) THEN 315.0 " +
+      "END"
+    end
+  end
+end
diff --git a/lib/geocoder/stores/active_record.rb b/lib/geocoder/stores/active_record.rb
index 819e987c4480e622f079ce599df26df26151d81e..9255a4cb0af88c6ecfa5238f426a12a1697d57cf 100644
--- a/lib/geocoder/stores/active_record.rb
+++ b/lib/geocoder/stores/active_record.rb
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+require 'geocoder/sql'
 require 'geocoder/stores/base'
 
 ##
@@ -33,14 +35,16 @@ module Geocoder::Store
         #
         scope :near, lambda{ |location, *args|
           latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
-          if latitude and longitude
+          if Geocoder::Calculations.coordinates_present?(latitude, longitude)
             near_scope_options(latitude, longitude, *args)
           else
-            where(:id => false) # no results if no lat/lon given
+            # If no lat/lon given we don't want any results, but we still
+            # need distance and bearing columns so you can add, for example:
+            # .order("distance")
+            select(select_clause(nil, "NULL", "NULL")).where(false_condition)
           end
         }
 
-
         ##
         # Find all objects within the area of a given bounding box.
         # Bounds must be an array of locations specifying the southwest
@@ -49,14 +53,15 @@ module Geocoder::Store
         #
         scope :within_bounding_box, lambda{ |bounds|
           sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
-          return where(:id => false) unless sw_lat && sw_lng && ne_lat && ne_lng
-          spans = "#{geocoder_options[:latitude]} BETWEEN #{sw_lat} AND #{ne_lat} AND "
-          spans << if sw_lng > ne_lng   # Handle a box that spans 180
-            "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND 180 OR #{geocoder_options[:longitude]} BETWEEN -180 AND #{ne_lng}"
+          if sw_lat && sw_lng && ne_lat && ne_lng
+            {:conditions => Geocoder::Sql.within_bounding_box(
+              sw_lat, sw_lng, ne_lat, ne_lng,
+              full_column_name(geocoder_options[:latitude]),
+              full_column_name(geocoder_options[:longitude])
+            )}
           else
-            "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND #{ne_lng}"
+            select(select_clause(nil, "NULL", "NULL")).where(false_condition)
           end
-          { :conditions => spans }
         }
       end
     end
@@ -68,178 +73,152 @@ module Geocoder::Store
 
       def distance_from_sql(location, *args)
         latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
-        distance_from_sql_options(latitude, longitude, *args) if latitude and longitude
+        if Geocoder::Calculations.coordinates_present?(latitude, longitude)
+          distance_sql(latitude, longitude, *args)
+        end
       end
 
       private # ----------------------------------------------------------------
 
       ##
       # Get options hash suitable for passing to ActiveRecord.find to get
-      # records within a radius (in miles) of the given point.
+      # records within a radius (in kilometers) of the given point.
       # Options hash may include:
       #
-      # * +:units+   - <tt>:mi</tt> (default) or <tt>:km</tt>; to be used
+      # * +:units+   - <tt>:mi</tt> or <tt>:km</tt>; to be used.
       #   for interpreting radius as well as the +distance+ attribute which
-      #   is added to each found nearby object
-      # * +:bearing+ - <tt>:linear</tt> (default) or <tt>:spherical</tt>;
+      #   is added to each found nearby object.
+      #   Use Geocoder.configure[:units] to configure default units.
+      # * +:bearing+ - <tt>:linear</tt> or <tt>:spherical</tt>.
       #   the method to be used for calculating the bearing (direction)
       #   between the given point and each found nearby point;
-      #   set to false for no bearing calculation
-      # * +:select+  - string with the SELECT SQL fragment (e.g. “id, name”)
-      # * +:order+   - column(s) for ORDER BY SQL clause; default is distance
-      # * +:exclude+ - an object to exclude (used by the +nearbys+ method)
+      #   set to false for no bearing calculation. Use
+      #   Geocoder.configure[:distances] to configure default calculation method.
+      # * +:select+          - string with the SELECT SQL fragment (e.g. “id, name”)
+      # * +:select_distance+ - whether to include the distance alias in the
+      #                        SELECT SQL fragment (e.g. <formula> AS distance)
+      # * +:select_bearing+  - like +:select_distance+ but for bearing.
+      # * +:order+           - column(s) for ORDER BY SQL clause; default is distance;
+      #                        set to false or nil to omit the ORDER BY clause
+      # * +:exclude+         - an object to exclude (used by the +nearbys+ method)
       #
       def near_scope_options(latitude, longitude, radius = 20, options = {})
-        if connection.adapter_name.match /sqlite/i
-          approx_near_scope_options(latitude, longitude, radius, options)
-        else
-          full_near_scope_options(latitude, longitude, radius, options)
-        end
-      end
+        options[:units] ||= (geocoder_options[:units] || Geocoder.config.units)
+        select_distance = options.fetch(:select_distance, true)
+        options[:order] = "" if !select_distance && !options.include?(:order)
+        select_bearing = options.fetch(:select_bearing, true)
+        bearing = bearing_sql(latitude, longitude, options)
+        distance = distance_sql(latitude, longitude, options)
+
+        b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
+        args = b + [
+          full_column_name(geocoder_options[:latitude]),
+          full_column_name(geocoder_options[:longitude])
+        ]
+        bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args)
 
-      def distance_from_sql_options(latitude, longitude, options = {})
-        if connection.adapter_name.match /sqlite/i
-          approx_distance_from_sql(latitude, longitude, options)
+        if using_sqlite?
+          conditions = bounding_box_conditions
         else
-          full_distance_from_sql(latitude, longitude, options)
+          conditions = [bounding_box_conditions + " AND #{distance} <= ?", radius]
         end
+        {
+          :select => select_clause(options[:select],
+                                   select_distance ? distance : nil,
+                                   select_bearing ? bearing : nil),
+          :conditions => add_exclude_condition(conditions, options[:exclude]),
+          :order => options.include?(:order) ? options[:order] : "distance ASC"
+        }
       end
 
       ##
-      # Scope options hash for use with a database that supports POWER(),
-      # SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
-      # ATAN2(), DEGREES(), and RADIANS().
-      #
-      # Bearing calculation based on:
-      # http://www.beginningspatial.com/calculating_bearing_one_point_another
+      # SQL for calculating distance based on the current database's
+      # capabilities (trig functions?).
       #
-      def full_near_scope_options(latitude, longitude, radius, options)
-        lat_attr = geocoder_options[:latitude]
-        lon_attr = geocoder_options[:longitude]
-        options[:bearing] = :linear unless options.include?(:bearing)
-        bearing = case options[:bearing]
-        when :linear
-          "CAST(" +
-            "DEGREES(ATAN2( " +
-              "RADIANS(#{lon_attr} - #{longitude}), " +
-              "RADIANS(#{lat_attr} - #{latitude})" +
-            ")) + 360 " +
-          "AS decimal) % 360"
-        when :spherical
-          "CAST(" +
-            "DEGREES(ATAN2( " +
-              "SIN(RADIANS(#{lon_attr} - #{longitude})) * " +
-              "COS(RADIANS(#{lat_attr})), (" +
-                "COS(RADIANS(#{latitude})) * SIN(RADIANS(#{lat_attr}))" +
-              ") - (" +
-                "SIN(RADIANS(#{latitude})) * COS(RADIANS(#{lat_attr})) * " +
-                "COS(RADIANS(#{lon_attr} - #{longitude}))" +
-              ")" +
-            ")) + 360 " +
-          "AS decimal) % 360"
-        end
-
-        distance = full_distance_from_sql(latitude, longitude, options)
-        conditions = ["#{distance} <= ?", radius]
-        default_near_scope_options(latitude, longitude, radius, options).merge(
-          :select => "#{options[:select] || "#{table_name}.*"}, " +
-            "#{distance} AS distance" +
-            (bearing ? ", #{bearing} AS bearing" : ""),
-          :conditions => add_exclude_condition(conditions, options[:exclude])
+      def distance_sql(latitude, longitude, options = {})
+        method_prefix = using_sqlite? ? "approx" : "full"
+        Geocoder::Sql.send(
+          method_prefix + "_distance",
+          latitude, longitude,
+          full_column_name(geocoder_options[:latitude]),
+          full_column_name(geocoder_options[:longitude]),
+          options
         )
       end
 
-
-      # Distance calculations based on the excellent tutorial at:
-      # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
-
-      def full_distance_from_sql(latitude, longitude, options)
-        lat_attr = geocoder_options[:latitude]
-        lon_attr = geocoder_options[:longitude]
-
-        earth = Geocoder::Calculations.earth_radius(options[:units] || :mi)
-
-        "#{earth} * 2 * ASIN(SQRT(" +
-          "POWER(SIN((#{latitude} - #{table_name}.#{lat_attr}) * PI() / 180 / 2), 2) + " +
-          "COS(#{latitude} * PI() / 180) * COS(#{table_name}.#{lat_attr} * PI() / 180) * " +
-          "POWER(SIN((#{longitude} - #{table_name}.#{lon_attr}) * PI() / 180 / 2), 2) ))"
-      end
-
-      def approx_distance_from_sql(latitude, longitude, options)
-        lat_attr = geocoder_options[:latitude]
-        lon_attr = geocoder_options[:longitude]
-
-        dx = Geocoder::Calculations.longitude_degree_distance(30, options[:units] || :mi)
-        dy = Geocoder::Calculations.latitude_degree_distance(options[:units] || :mi)
-
-        # sin of 45 degrees = average x or y component of vector
-        factor = Math.sin(Math::PI / 4)
-
-        "(#{dy} * ABS(#{table_name}.#{lat_attr} - #{latitude}) * #{factor}) + " +
-          "(#{dx} * ABS(#{table_name}.#{lon_attr} - #{longitude}) * #{factor})"
-      end
-
       ##
-      # Scope options hash for use with a database without trigonometric
-      # functions, like SQLite. Approach is to find objects within a square
-      # rather than a circle, so results are very approximate (will include
-      # objects outside the given radius).
+      # SQL for calculating bearing based on the current database's
+      # capabilities (trig functions?).
       #
-      # Distance and bearing calculations are *extremely inaccurate*. They
-      # only exist for interface consistency--not intended for production!
-      #
-      def approx_near_scope_options(latitude, longitude, radius, options)
-        lat_attr = geocoder_options[:latitude]
-        lon_attr = geocoder_options[:longitude]
-        options[:bearing] = :linear unless options.include?(:bearing)
+      def bearing_sql(latitude, longitude, options = {})
+        if !options.include?(:bearing)
+          options[:bearing] = Geocoder.config.distances
+        end
         if options[:bearing]
-          bearing = "CASE " +
-            "WHEN (#{lat_attr} >= #{latitude} AND #{lon_attr} >= #{longitude}) THEN  45.0 " +
-            "WHEN (#{lat_attr} <  #{latitude} AND #{lon_attr} >= #{longitude}) THEN 135.0 " +
-            "WHEN (#{lat_attr} <  #{latitude} AND #{lon_attr} <  #{longitude}) THEN 225.0 " +
-            "WHEN (#{lat_attr} >= #{latitude} AND #{lon_attr} <  #{longitude}) THEN 315.0 " +
-          "END"
-        else
-          bearing = false
+          method_prefix = using_sqlite? ? "approx" : "full"
+          Geocoder::Sql.send(
+            method_prefix + "_bearing",
+            latitude, longitude,
+            full_column_name(geocoder_options[:latitude]),
+            full_column_name(geocoder_options[:longitude]),
+            options
+          )
         end
-
-        distance = approx_distance_from_sql(latitude, longitude, options)
-
-        b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
-        conditions = [
-          "#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
-          [b[0], b[2], b[1], b[3]
-        ]
-        default_near_scope_options(latitude, longitude, radius, options).merge(
-          :select => "#{options[:select] || "#{table_name}.*"}, " +
-            "#{distance} AS distance" +
-            (bearing ? ", #{bearing} AS bearing" : ""),
-          :conditions => add_exclude_condition(conditions, options[:exclude])
-        )
       end
 
       ##
-      # Options used for any near-like scope.
+      # Generate the SELECT clause.
       #
-      def default_near_scope_options(latitude, longitude, radius, options)
-        {
-          :order  => options[:order] || "distance",
-          :limit  => options[:limit],
-          :offset => options[:offset]
-        }
+      def select_clause(columns, distance = nil, bearing = nil)
+        if columns == :id_only
+          return full_column_name(primary_key)
+        elsif columns == :geo_only
+          clause = ""
+        else
+          clause = (columns || full_column_name("*"))
+        end
+        if distance
+          clause += ", " unless clause.empty?
+          clause += "#{distance} AS distance"
+        end
+        if bearing
+          clause += ", " unless clause.empty?
+          clause += "#{bearing} AS bearing"
+        end
+        clause
       end
 
       ##
       # Adds a condition to exclude a given object by ID.
-      # The given conditions MUST be an array.
+      # Expects conditions as an array or string. Returns array.
       #
       def add_exclude_condition(conditions, exclude)
+        conditions = [conditions] if conditions.is_a?(String)
         if exclude
-          conditions[0] << " AND #{table_name}.id != ?"
+          conditions[0] << " AND #{full_column_name(primary_key)} != ?"
           conditions << exclude.id
         end
         conditions
       end
+
+      def using_sqlite?
+        connection.adapter_name.match /sqlite/i
+      end
+
+      ##
+      # Value which can be passed to where() to produce no results.
+      #
+      def false_condition
+        using_sqlite? ? 0 : "false"
+      end
+
+      ##
+      # Prepend table name if column name doesn't already contain one.
+      #
+      def full_column_name(column)
+        column = column.to_s
+        column.include?(".") ? column : [table_name, column].join(".")
+      end
     end
 
     ##
@@ -250,8 +229,8 @@ module Geocoder::Store
       do_lookup(false) do |o,rs|
         if r = rs.first
           unless r.latitude.nil? or r.longitude.nil?
-            o.send :write_attribute, self.class.geocoder_options[:latitude],  r.latitude
-            o.send :write_attribute, self.class.geocoder_options[:longitude], r.longitude
+            o.__send__  "#{self.class.geocoder_options[:latitude]}=",  r.latitude
+            o.__send__  "#{self.class.geocoder_options[:longitude]}=", r.longitude
           end
           r.coordinates
         end
@@ -268,7 +247,7 @@ module Geocoder::Store
       do_lookup(true) do |o,rs|
         if r = rs.first
           unless r.address.nil?
-            o.send :write_attribute, self.class.geocoder_options[:fetched_address], r.address
+            o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
           end
           r.address
         end
diff --git a/lib/geocoder/stores/base.rb b/lib/geocoder/stores/base.rb
index f7726c645153738566b4d8468e2e506720a6704b..dfe1cfd256cce9a3a0dc47143e0950e73d46348d 100644
--- a/lib/geocoder/stores/base.rb
+++ b/lib/geocoder/stores/base.rb
@@ -20,9 +20,10 @@ module Geocoder
       # Calculate the distance from the object to an arbitrary point.
       # See Geocoder::Calculations.distance_between for ways of specifying
       # the point. Also takes a symbol specifying the units
-      # (:mi or :km; default is :mi).
+      # (:mi or :km; can be specified in Geocoder configuration).
       #
-      def distance_to(point, units = :mi)
+      def distance_to(point, units = nil)
+        units ||= self.class.geocoder_options[:units]
         return nil unless geocoded?
         Geocoder::Calculations.distance_between(
           to_coordinates, point, :units => units)
@@ -36,6 +37,7 @@ module Geocoder
       # ways of specifying the point.
       #
       def bearing_to(point, options = {})
+        options[:method] ||= self.class.geocoder_options[:method]
         return nil unless geocoded?
         Geocoder::Calculations.bearing_between(
           to_coordinates, point, options)
@@ -47,6 +49,7 @@ module Geocoder
       # ways of specifying the point.
       #
       def bearing_from(point, options = {})
+        options[:method] ||= self.class.geocoder_options[:method]
         return nil unless geocoded?
         Geocoder::Calculations.bearing_between(
           point, to_coordinates, options)
@@ -55,10 +58,11 @@ module Geocoder
       ##
       # Get nearby geocoded objects.
       # Takes the same options hash as the near class method (scope).
+      # Returns nil if the object is not geocoded.
       #
       def nearbys(radius = 20, options = {})
-        return [] unless geocoded?
-        options.merge!(:exclude => self)
+        return nil unless geocoded?
+        options.merge!(:exclude => self) unless send(self.class.primary_key).nil?
         self.class.near(self, radius, options)
       end
 
@@ -78,7 +82,6 @@ module Geocoder
         fail
       end
 
-
       private # --------------------------------------------------------------
 
       ##
@@ -114,3 +117,4 @@ module Geocoder
     end
   end
 end
+
diff --git a/lib/geocoder/stores/mongo_base.rb b/lib/geocoder/stores/mongo_base.rb
index 29054e486f25987c56cfa2dcda63d91c71bd6fc5..62a4d98f987f8a83c3a533a353c545ce2ebc0308 100644
--- a/lib/geocoder/stores/mongo_base.rb
+++ b/lib/geocoder/stores/mongo_base.rb
@@ -20,6 +20,7 @@ module Geocoder::Store
 
           radius  = args.size > 0 ? args.shift : 20
           options = args.size > 0 ? args.shift : {}
+          options[:units] ||= geocoder_options[:units]
 
           # Use BSON::OrderedHash if Ruby's hashes are unordered.
           # Conditions must be in order required by indexes (see mongo gem).
@@ -30,7 +31,7 @@ module Geocoder::Store
           conds[field] = empty.clone
           conds[field]["$nearSphere"]  = coords.reverse
           conds[field]["$maxDistance"] = \
-            Geocoder::Calculations.distance_to_radians(radius, options[:units] || :mi)
+            Geocoder::Calculations.distance_to_radians(radius, options[:units])
 
           if obj = options[:exclude]
             conds[:_id.ne] = obj.id
@@ -58,7 +59,7 @@ module Geocoder::Store
       do_lookup(false) do |o,rs|
         if r = rs.first
           unless r.coordinates.nil?
-            o.send :write_attribute, self.class.geocoder_options[:coordinates], r.coordinates.reverse
+            o.__send__ "#{self.class.geocoder_options[:coordinates]}=", r.coordinates.reverse
           end
           r.coordinates
         end
@@ -73,7 +74,7 @@ module Geocoder::Store
       do_lookup(true) do |o,rs|
         if r = rs.first
           unless r.address.nil?
-            o.send :write_attribute, self.class.geocoder_options[:fetched_address], r.address
+            o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
           end
           r.address
         end
@@ -81,3 +82,4 @@ module Geocoder::Store
     end
   end
 end
+
diff --git a/lib/geocoder/version.rb b/lib/geocoder/version.rb
index ef1e30beed4e1e726c0400949ebac4e75df50952..d56eefe6333abacfe9e1b7bdd8f611f45dc7e536 100644
--- a/lib/geocoder/version.rb
+++ b/lib/geocoder/version.rb
@@ -1,3 +1,3 @@
 module Geocoder
-  VERSION = "1.1.1"
+  VERSION = "1.1.6"
 end
diff --git a/lib/hash_recursive_merge.rb b/lib/hash_recursive_merge.rb
new file mode 100644
index 0000000000000000000000000000000000000000..163566ee13c2816b93999baf4c2bc5e493510170
--- /dev/null
+++ b/lib/hash_recursive_merge.rb
@@ -0,0 +1,74 @@
+# 
+# = 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/oauth_util.rb b/lib/oauth_util.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b479d3547d57813b2ea999795990a30a76e37045
--- /dev/null
+++ b/lib/oauth_util.rb
@@ -0,0 +1,112 @@
+# A utility for signing an url using OAuth in a way that's convenient for debugging
+# Note: the standard Ruby OAuth lib is here http://github.com/mojodna/oauth
+# Source: http://gist.github.com/383159
+# License: http://gist.github.com/375593
+# Usage: see example.rb below
+#
+# NOTE: This file has been modified from the original Gist:
+#
+# 1. Fix to prevent param-array conversion, as mentioned in Gist comment.
+# 2. Query string escaping has been changed. See:
+#   https://github.com/alexreisner/geocoder/pull/360
+#
+
+require 'uri'
+require 'cgi'
+require 'openssl'
+require 'base64'
+
+class OauthUtil
+
+  attr_accessor :consumer_key, :consumer_secret, :token, :token_secret, :req_method, 
+                :sig_method, :oauth_version, :callback_url, :params, :req_url, :base_str
+
+  def initialize
+    @consumer_key = ''
+    @consumer_secret = ''
+    @token = ''
+    @token_secret = ''
+    @req_method = 'GET'
+    @sig_method = 'HMAC-SHA1'
+    @oauth_version = '1.0'
+    @callback_url = ''
+  end
+
+  # openssl::random_bytes returns non-word chars, which need to be removed. using alt method to get length
+  # ref http://snippets.dzone.com/posts/show/491
+  def nonce
+    Array.new( 5 ) { rand(256) }.pack('C*').unpack('H*').first
+  end
+
+  def percent_encode( string )
+
+    # ref http://snippets.dzone.com/posts/show/1260
+    return URI.escape( string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]") ).gsub('*', '%2A')
+  end
+
+  # @ref http://oauth.net/core/1.0/#rfc.section.9.2
+  def signature
+    key = percent_encode( @consumer_secret ) + '&' + percent_encode( @token_secret )
+
+    # ref: http://blog.nathanielbibler.com/post/63031273/openssl-hmac-vs-ruby-hmac-benchmarks
+    digest = OpenSSL::Digest::Digest.new( 'sha1' )
+    hmac = OpenSSL::HMAC.digest( digest, key, @base_str )
+
+    # ref http://groups.google.com/group/oauth-ruby/browse_thread/thread/9110ed8c8f3cae81
+    Base64.encode64( hmac ).chomp.gsub( /\n/, '' )
+  end
+
+  # sort (very important as it affects the signature), concat, and percent encode
+  # @ref http://oauth.net/core/1.0/#rfc.section.9.1.1
+  # @ref http://oauth.net/core/1.0/#9.2.1
+  # @ref http://oauth.net/core/1.0/#rfc.section.A.5.1
+  def query_string
+    pairs = []
+    @params.sort.each { | key, val | 
+      pairs.push( "#{ CGI.escape(key.to_s).gsub(/%(5B|5D)/n) { [$1].pack('H*') } }=#{ CGI.escape(val.to_s) }" )
+    }
+    pairs.join '&'
+  end
+
+  # organize params & create signature
+  def sign( parsed_url )
+
+    @params = {
+      'oauth_consumer_key' => @consumer_key,
+      'oauth_nonce' => nonce,
+      'oauth_signature_method' => @sig_method,
+      'oauth_timestamp' => Time.now.to_i.to_s,
+      'oauth_version' => @oauth_version
+    }
+
+    # if url has query, merge key/values into params obj overwriting defaults
+    if parsed_url.query
+      CGI.parse( parsed_url.query ).each do |k,v|
+        if v.is_a?(Array) && v.count == 1
+          @params[k] = v.first
+        else
+          @params[k] = v
+        end
+      end
+    end
+
+    # @ref http://oauth.net/core/1.0/#rfc.section.9.1.2
+    @req_url = parsed_url.scheme + '://' + parsed_url.host + parsed_url.path
+
+    # create base str. make it an object attr for ez debugging
+    # ref http://oauth.net/core/1.0/#anchor14
+    @base_str = [ 
+      @req_method, 
+      percent_encode( req_url ), 
+
+      # normalization is just x-www-form-urlencoded
+      percent_encode( query_string ) 
+
+    ].join( '&' )
+
+    # add signature
+    @params[ 'oauth_signature' ] = signature
+
+    return self
+  end
+end
diff --git a/lib/tasks/geocoder.rake b/lib/tasks/geocoder.rake
index f235e1b51b2cb047021042a5d4719f3e4233ab40..33ed4829e1ed76c624106d0fae5deefef82378f7 100644
--- a/lib/tasks/geocoder.rake
+++ b/lib/tasks/geocoder.rake
@@ -3,10 +3,23 @@ namespace :geocode do
   task :all => :environment do
     class_name = ENV['CLASS'] || ENV['class']
     raise "Please specify a CLASS (model)" unless class_name
-    klass = Object.const_get(class_name)
+    klass = class_from_string(class_name)
 
     klass.not_geocoded.each do |obj|
       obj.geocode; obj.save
     end
   end
 end
+
+##
+# Get a class object from the string given in the shell environment.
+# Similar to ActiveSupport's +constantize+ method.
+#
+def class_from_string(class_name)
+  parts = class_name.split("::")
+  constant = Object
+  parts.each do |part|
+    constant = constant.const_get(part)
+  end
+  constant
+end
diff --git a/test/active_record_test.rb b/test/active_record_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d2d2316b085a5584935385d0379e5a7c1072fc3
--- /dev/null
+++ b/test/active_record_test.rb
@@ -0,0 +1,15 @@
+# encoding: utf-8
+require 'test_helper'
+
+class ActiveRecordTest < Test::Unit::TestCase
+
+  def test_exclude_condition_when_model_has_a_custom_primary_key
+    venue = VenuePlus.new(*venue_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( /#{VenuePlus.primary_key}/, conditions.join)
+  end
+
+end
diff --git a/test/cache_test.rb b/test/cache_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..edd45bbfae62464244570da05ad58195b5ef1e31
--- /dev/null
+++ b/test/cache_test.rb
@@ -0,0 +1,19 @@
+# encoding: utf-8
+require 'test_helper'
+
+class CacheTest < Test::Unit::TestCase
+
+  def test_second_occurrence_of_request_is_cache_hit
+    Geocoder.configure(:cache => {})
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      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
+end
diff --git a/test/calculations_test.rb b/test/calculations_test.rb
index 25343cb7a5f3ae3f53153e9bf3fafe57fa1bcdde..104d237ffcfe3614099b6ea7bdafc4402044b11f 100644
--- a/test/calculations_test.rb
+++ b/test/calculations_test.rb
@@ -2,7 +2,12 @@
 require 'test_helper'
 
 class CalculationsTest < Test::Unit::TestCase
-
+  def setup
+    Geocoder.configure(
+      :units => :mi,
+      :distances => :linear
+    )
+  end
 
   # --- degree distance ---
 
@@ -144,4 +149,47 @@ class CalculationsTest < Test::Unit::TestCase
     l = Landmark.new(*landmark_params(:msg))
     assert_equal l.bearing_from([50,-86.1]), l.bearing_to([50,-86.1]) - 180
   end
+
+  def test_extract_coordinates
+    result = Geocoder::Calculations.extract_coordinates([ nil, nil ])
+    assert is_nan_coordinates?(result)
+
+    result = Geocoder::Calculations.extract_coordinates([ 1.0 / 3, 2.0 / 3 ])
+    assert_in_delta 1.0 / 3, result.first, 1E-5
+    assert_in_delta 2.0 / 3, result.last, 1E-5
+
+    result = Geocoder::Calculations.extract_coordinates(nil)
+    assert is_nan_coordinates?(result)
+
+    result = Geocoder::Calculations.extract_coordinates('')
+    assert is_nan_coordinates?(result)
+
+    result = Geocoder::Calculations.extract_coordinates([ 'nix' ])
+    assert is_nan_coordinates?(result)
+
+    o = Object.new
+    result = Geocoder::Calculations.extract_coordinates(o)
+    assert is_nan_coordinates?(result)
+
+    def o.to_coordinates
+      [ 1.0 / 3, 2.0 / 3 ]
+    end
+    result = Geocoder::Calculations.extract_coordinates(o)
+    assert_in_delta 1.0 / 3, result.first, 1E-5
+    assert_in_delta 2.0 / 3, result.last, 1E-5
+  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
+
+  def test_extract_coordinates
+    coords = [-23,47]
+    l = Landmark.new("Madagascar", coords[0], coords[1])
+    assert_equal coords, Geocoder::Calculations.extract_coordinates(l)
+    assert_equal coords, Geocoder::Calculations.extract_coordinates(coords)
+  end
 end
diff --git a/test/configuration_test.rb b/test/configuration_test.rb
index 4c4e668186218874e10611f24cd2f899f667ab79..7aad1d7f9059c0813e360c8528de5b5503f83290 100644
--- a/test/configuration_test.rb
+++ b/test/configuration_test.rb
@@ -2,12 +2,77 @@
 require 'test_helper'
 
 class ConfigurationTest < Test::Unit::TestCase
+  def setup
+    Geocoder::Configuration.set_defaults
+  end
 
   def test_exception_raised_on_bad_lookup_config
-    Geocoder::Configuration.lookup = :stoopid
+    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_setting_with_block_syntax
+    orig = $VERBOSE; $VERBOSE = nil
+    Geocoder.configure do |config|
+      config.units = :test
+    end
+    assert_equal :test, Geocoder.config.units
+  ensure
+    $VERBOSE = orig
+  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_model_configuration
+    Landmark.reverse_geocoded_by :latitude, :longitude, :method => :spherical, :units => :km
+    assert_equal :km,        Landmark.geocoder_options[:units]
+    assert_equal :spherical, Landmark.geocoder_options[:method]
+
+    v = Landmark.new(*landmark_params(:msg))
+    v.latitude  = 0
+    v.longitude = 0
+    assert_equal 111, v.distance_to([0,1]).round
+    v.latitude  = 40.750354
+    v.longitude = -73.993371
+    assert_equal 136, v.bearing_from([50,-85]).round
+  end
+
+  def test_configuration_chain
+    v = Landmark.new(*landmark_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
+    Landmark.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
 end
diff --git a/test/custom_block_test.rb b/test/custom_block_test.rb
index 8fab62e819aa6ff05a1b706e185ab7aba92008fc..0f4789d163dc80ab40016016571848f623525499 100644
--- a/test/custom_block_test.rb
+++ b/test/custom_block_test.rb
@@ -29,3 +29,4 @@ class CustomBlockTest < Test::Unit::TestCase
     assert_nil e.address
   end
 end
+
diff --git a/test/error_handling_test.rb b/test/error_handling_test.rb
index ca4cec56c88727d33c796c57b1a19310d7e7fb85..b4f28774ea90b6624a005b138ee5ce4409411038 100644
--- a/test/error_handling_test.rb
+++ b/test/error_handling_test.rb
@@ -4,35 +4,39 @@ require 'test_helper'
 class ErrorHandlingTest < Test::Unit::TestCase
 
   def teardown
-    Geocoder::Configuration.always_raise = []
+    Geocoder.configure(:always_raise => [])
   end
 
   def test_does_not_choke_on_timeout
     # keep test output clean: suppress timeout warning
     orig = $VERBOSE; $VERBOSE = nil
-    all_lookups.each do |l|
-      Geocoder::Configuration.lookup = l
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      Geocoder.configure(:lookup => l)
+      set_api_key!(l)
       assert_nothing_raised { Geocoder.search("timeout") }
     end
+  ensure
     $VERBOSE = orig
   end
 
   def test_always_raise_timeout_error
-    Geocoder::Configuration.always_raise = [TimeoutError]
-    all_lookups.each do |l|
-      lookup = Geocoder.send(:get_lookup, l)
+    Geocoder.configure(:always_raise => [TimeoutError])
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
       assert_raises TimeoutError do
-        lookup.send(:results, "timeout")
+        lookup.send(:results, Geocoder::Query.new("timeout"))
       end
     end
   end
 
   def test_always_raise_socket_error
-    Geocoder::Configuration.always_raise = [SocketError]
-    all_lookups.each do |l|
-      lookup = Geocoder.send(:get_lookup, l)
+    Geocoder.configure(:always_raise => [SocketError])
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
       assert_raises SocketError do
-        lookup.send(:results, "socket_error")
+        lookup.send(:results, Geocoder::Query.new("socket_error"))
       end
     end
   end
diff --git a/test/fixtures/bing_invalid_key b/test/fixtures/bing_invalid_key
new file mode 100644
index 0000000000000000000000000000000000000000..d34e1e93ea9ee2fc878804fa9a2fea348b87e074
--- /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.json b/test/fixtures/bing_madison_square_garden
similarity index 100%
rename from test/fixtures/bing_madison_square_garden.json
rename to test/fixtures/bing_madison_square_garden
diff --git a/test/fixtures/bing_no_results.json b/test/fixtures/bing_no_results
similarity index 100%
rename from test/fixtures/bing_no_results.json
rename to test/fixtures/bing_no_results
diff --git a/test/fixtures/bing_reverse.json b/test/fixtures/bing_reverse
similarity index 100%
rename from test/fixtures/bing_reverse.json
rename to test/fixtures/bing_reverse
diff --git a/test/fixtures/freegeoip_74_200_247_59.json b/test/fixtures/freegeoip_74_200_247_59
similarity index 100%
rename from test/fixtures/freegeoip_74_200_247_59.json
rename to test/fixtures/freegeoip_74_200_247_59
diff --git a/test/fixtures/freegeoip_no_results.json b/test/fixtures/freegeoip_no_results
similarity index 100%
rename from test/fixtures/freegeoip_no_results.json
rename to test/fixtures/freegeoip_no_results
diff --git a/test/fixtures/geocoder_ca_madison_square_garden.json b/test/fixtures/geocoder_ca_madison_square_garden
similarity index 100%
rename from test/fixtures/geocoder_ca_madison_square_garden.json
rename to test/fixtures/geocoder_ca_madison_square_garden
diff --git a/test/fixtures/geocoder_ca_no_results.json b/test/fixtures/geocoder_ca_no_results
similarity index 100%
rename from test/fixtures/geocoder_ca_no_results.json
rename to test/fixtures/geocoder_ca_no_results
diff --git a/test/fixtures/geocoder_ca_reverse.json b/test/fixtures/geocoder_ca_reverse
similarity index 100%
rename from test/fixtures/geocoder_ca_reverse.json
rename to test/fixtures/geocoder_ca_reverse
diff --git a/test/fixtures/google_garbage.json b/test/fixtures/google_garbage
similarity index 100%
rename from test/fixtures/google_garbage.json
rename to test/fixtures/google_garbage
diff --git a/test/fixtures/google_madison_square_garden.json b/test/fixtures/google_madison_square_garden
similarity index 100%
rename from test/fixtures/google_madison_square_garden.json
rename to test/fixtures/google_madison_square_garden
diff --git a/test/fixtures/google_no_city_data.json b/test/fixtures/google_no_city_data
similarity index 100%
rename from test/fixtures/google_no_city_data.json
rename to test/fixtures/google_no_city_data
diff --git a/test/fixtures/google_no_locality.json b/test/fixtures/google_no_locality
similarity index 100%
rename from test/fixtures/google_no_locality.json
rename to test/fixtures/google_no_locality
diff --git a/test/fixtures/google_no_results.json b/test/fixtures/google_no_results
similarity index 100%
rename from test/fixtures/google_no_results.json
rename to test/fixtures/google_no_results
diff --git a/test/fixtures/mapquest_madison_square_garden b/test/fixtures/mapquest_madison_square_garden
new file mode 100644
index 0000000000000000000000000000000000000000..86c135bac459db715783f28a07b793cc8820ff19
--- /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 0000000000000000000000000000000000000000..0cfc3e76e385c41ecb2f9cfcea589e3f44ba38ab
--- /dev/null
+++ b/test/fixtures/mapquest_no_results
@@ -0,0 +1,7 @@
+{
+  "results": [
+    {
+    "locations": []
+    }
+  ]
+}
diff --git a/test/fixtures/maxmind_24_24_24_21 b/test/fixtures/maxmind_24_24_24_21
new file mode 100644
index 0000000000000000000000000000000000000000..f4cd62e01d3d617a216c8f5b6af90b313636da09
--- /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 0000000000000000000000000000000000000000..b9aee130e818f620dea7edc6ce110478578ab20c
--- /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 0000000000000000000000000000000000000000..29ecfb21b570c06c82348010be3f055d60f5bbb1
--- /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 0000000000000000000000000000000000000000..14939030ea18388814557a2ffa162beab75ea6f1
--- /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 0000000000000000000000000000000000000000..9eb1bd01172e116e6bc07ec891c969420b03ee4e
--- /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_invalid_key b/test/fixtures/maxmind_invalid_key
new file mode 100644
index 0000000000000000000000000000000000000000..e216586596bffa02427197ab20653bc8f5d04529
--- /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 0000000000000000000000000000000000000000..c1d1bc2d3195d6084a0e152a2a2dbf193382bc04
--- /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/nominatim_madison_square_garden.json b/test/fixtures/nominatim_madison_square_garden
similarity index 100%
rename from test/fixtures/nominatim_madison_square_garden.json
rename to test/fixtures/nominatim_madison_square_garden
diff --git a/test/fixtures/nominatim_no_results.json b/test/fixtures/nominatim_no_results
similarity index 100%
rename from test/fixtures/nominatim_no_results.json
rename to test/fixtures/nominatim_no_results
diff --git a/test/fixtures/yahoo_error b/test/fixtures/yahoo_error
new file mode 100644
index 0000000000000000000000000000000000000000..7dbfdfeabeaa611d00af6ef756118795619703a1
--- /dev/null
+++ b/test/fixtures/yahoo_error
@@ -0,0 +1 @@
+{"bossresponse":{"responsecode":"6000","reason":"internal error"}}
diff --git a/test/fixtures/yahoo_garbage.json b/test/fixtures/yahoo_garbage.json
deleted file mode 100644
index 8e8970fd26048791ff42b17e0543ff1c4beb0710..0000000000000000000000000000000000000000
--- a/test/fixtures/yahoo_garbage.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
-  "ResultSet":{
-    "version":"1.0",
-    "Error":0,
-    "ErrorMessage":"No error",
-    "Locale":"us_US",
-    "Quality":87,
-    "Found":1,
-    "Results":[{
-      "quality":9,
-      "latitude":"55.008390",
-      "longitude":"-5.822485",
-      "offsetlat":"54.314072",
-      "offsetlon":"-2.230010",
-      "radius":1145100,
-      "boundingbox":{
-        "north":"60.854691",
-        "south":"49.162090",
-        "east":"1.768960",
-        "west":"-13.413930"
-      },
-      "name":"",
-      "line1":"",
-      "line2":"",
-      "line3":"",
-      "line4":"United Kingdom",
-      "cross":"",
-      "house":"",
-      "street":"",
-      "xstreet":"",
-      "unittype":"",
-      "unit":"",
-      "postal":"",
-      "neighborhood":"",
-      "city":"",
-      "county":"",
-      "state":"",
-      "country":"United Kingdom",
-      "countrycode":"GB",
-      "statecode":"",
-      "countycode":"",
-      "timezone":"Europe/London",
-      "areacode":"",
-      "uzip":"",
-      "hash":"",
-      "woeid":23424975,
-      "woetype":12
-    }]
-  }
-}
diff --git a/test/fixtures/yahoo_invalid_key b/test/fixtures/yahoo_invalid_key
new file mode 100644
index 0000000000000000000000000000000000000000..c9b7319b05361287a7829d884d051fb397f36e4d
--- /dev/null
+++ b/test/fixtures/yahoo_invalid_key
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='UTF-8'?>\n<yahoo:error xmlns:yahoo='http://yahooapis.com/v1/base.rng'\n  xml:lang='en-US'>
+  <yahoo:description>Please provide valid credentials. OAuth oauth_problem="consumer_key_unknown", realm="yahooapis.com"</yahoo:description>\n</yahoo:error>
diff --git a/test/fixtures/yahoo_madison_square_garden b/test/fixtures/yahoo_madison_square_garden
new file mode 100644
index 0000000000000000000000000000000000000000..24161f60b57f58f99c68ec9e99f648c89ee08097
--- /dev/null
+++ b/test/fixtures/yahoo_madison_square_garden
@@ -0,0 +1,52 @@
+{
+  "bossresponse": {
+    "responsecode": "200",
+    "placefinder": {
+      "start": "0",
+      "count": "1",
+      "request": "flags=JXTSR&location=Madison%20Square%20Garden%2C%20NY%2C%20NY&%unsafe%appid=%5B%22dj0yJmk9ZmZ5NXFrNGhNcEthJmQ9WVdrOVFUSlhPV2x1TjJVbWNHbzlORE0wT0RFME9UWXkmcz1jb25zdW1lcnNlY3JldCZ4PTAy%22%2C%20%22b57b1b98eb21f171231f5b441cba505261d6c9bb%22%5D&gflags=AC&locale=en_US",
+      "results": [
+        {
+          "quality": "90",
+          "latitude": "40.750381",
+          "longitude": "-73.993988",
+          "offsetlat": "40.750381",
+          "offsetlon": "-73.993988",
+          "radius": "400",
+          "boundingbox": {
+            "north": "40.750832",
+            "south": "40.749931",
+            "east": "-73.993393",
+            "west": "-73.994591"
+          },
+          "name": "Madison Square Garden",
+          "line1": "Madison Square Garden",
+          "line2": "New York, NY 10001",
+          "line3": "",
+          "line4": "United States",
+          "cross": "",
+          "house": "",
+          "street": "",
+          "xstreet": "",
+          "unittype": "",
+          "unit": "",
+          "postal": "10001",
+          "neighborhood": "Garment District|Midtown|Midtown West|Manhattan",
+          "city": "New York",
+          "county": "New York County",
+          "state": "New York",
+          "country": "United States",
+          "countrycode": "US",
+          "statecode": "NY",
+          "countycode": "",
+          "timezone": "America/New_York",
+          "areacode": "212",
+          "uzip": "10001",
+          "hash": "",
+          "woeid": "23617041",
+          "woetype": "20"
+        }
+      ]
+    }
+  }
+}
diff --git a/test/fixtures/yahoo_madison_square_garden.json b/test/fixtures/yahoo_madison_square_garden.json
deleted file mode 100644
index 6e54b8e7269281ca7dcf02663361b158f6edd7eb..0000000000000000000000000000000000000000
--- a/test/fixtures/yahoo_madison_square_garden.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-  "ResultSet":{
-    "version":"1.0",
-    "Error":0,
-    "ErrorMessage":"No error",
-    "Locale":"us_US",
-    "Quality":90,
-    "Found":1,
-    "Results":[{
-      "quality":90,
-      "latitude":"40.750381",
-      "longitude":"-73.993988",
-      "offsetlat":"40.750381",
-      "offsetlon":"-73.993988",
-      "radius":100,
-      "name":"Madison Square Garden",
-      "line1":"Madison Square Garden",
-      "line2":"New York, NY  10001",
-      "line3":"",
-      "line4":"United States",
-      "house":"",
-      "street":"",
-      "xstreet":"",
-      "unittype":"",
-      "unit":"",
-      "postal":"10001",
-      "neighborhood":"",
-      "city":"New York",
-      "county":"New York County",
-      "state":"New York",
-      "country":"United States",
-      "countrycode":"US",
-      "statecode":"NY",
-      "countycode":"",
-      "uzip":"10001",
-      "hash":"",
-      "woeid":23617041,
-      "woetype":20,
-      "cross":"",
-      "timezone":"America/New_York",
-      "neighborhood":"Garment District|Midtown|Midtown West|Manhattan",
-      "areacode":"212",
-      "boundingbox":{"north":"40.750832","south":"40.749931","east":"-73.993393","west":"-73.994591"}
-    }]
-  }
-}
diff --git a/test/fixtures/yahoo_no_results b/test/fixtures/yahoo_no_results
new file mode 100644
index 0000000000000000000000000000000000000000..0c65fddc4a9dd4aa3791c7ec19ef75ba977837e8
--- /dev/null
+++ b/test/fixtures/yahoo_no_results
@@ -0,0 +1,10 @@
+{
+  "bossresponse": {
+    "responsecode": "200",
+    "placefinder": {
+      "start": "0",
+      "count": "0",
+      "request": "flags=JXTSR&location=asdfasdf28394782sdfj2983&%unsafe%appid=%5B%22dj0yJmk9ZmZ5NXFrNGhNcEthJmQ9WVdrOVFUSlhPV2x1TjJVbWNHbzlORE0wT0RFME9UWXkmcz1jb25zdW1lcnNlY3JldCZ4PTAy%22%2C%20%22b57b1b98eb21f171231f5b441cba505261d6c9bb%22%5D&gflags=AC&locale=en_US"
+    }
+  }
+}
diff --git a/test/fixtures/yahoo_no_results.json b/test/fixtures/yahoo_no_results.json
deleted file mode 100644
index e97865dfcf60fb4e970caac77d2187f3eadf640d..0000000000000000000000000000000000000000
--- a/test/fixtures/yahoo_no_results.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "ResultSet":{
-    "version":"1.0",
-    "Error":0,
-    "ErrorMessage":"No error",
-    "Locale":"us_US",
-    "Quality":10,
-    "Found":0
-  }
-}
diff --git a/test/fixtures/yahoo_over_limit b/test/fixtures/yahoo_over_limit
new file mode 100644
index 0000000000000000000000000000000000000000..7dddadf5f0d5e75bae7fd245d34dac41012e23aa
--- /dev/null
+++ b/test/fixtures/yahoo_over_limit
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?> <yahoo:error xmlns:yahoo="http://yahooapis.com/v1/base.rng\ <http://yahooapis.com/v1/base.rng%5C>" xml:lang="en-US"> 
+          <yahoo:description>Rate Limit Exceeded</yahoo:description> <yahoo:detail>Key has exceeded its configured rate limit.</yahoo:detail> </yahoo:error>
diff --git a/test/fixtures/yandex_invalid_key.json b/test/fixtures/yandex_invalid_key
similarity index 100%
rename from test/fixtures/yandex_invalid_key.json
rename to test/fixtures/yandex_invalid_key
diff --git a/test/fixtures/yandex_kremlin.json b/test/fixtures/yandex_kremlin
similarity index 100%
rename from test/fixtures/yandex_kremlin.json
rename to test/fixtures/yandex_kremlin
diff --git a/test/fixtures/yandex_no_results.json b/test/fixtures/yandex_no_results
similarity index 100%
rename from test/fixtures/yandex_no_results.json
rename to test/fixtures/yandex_no_results
diff --git a/test/https_test.rb b/test/https_test.rb
index 58219d33cc8f013746583b0d20a699e93b534ad8..dd6d02040aca3e132007d85ccd2b02e82a010098 100644
--- a/test/https_test.rb
+++ b/test/https_test.rb
@@ -4,13 +4,13 @@ require 'test_helper'
 class HttpsTest < Test::Unit::TestCase
 
   def test_uses_https_for_secure_query
-    Geocoder::Configuration.use_https = true
+    Geocoder.configure(:use_https => true)
     g = Geocoder::Lookup::Google.new
-    assert_match /^https:/, g.send(:query_url, {:a => 1, :b => 2})
+    assert_match /^https:/, g.query_url(Geocoder::Query.new("test"))
   end
 
   def test_uses_http_by_default
     g = Geocoder::Lookup::Google.new
-    assert_match /^http:/, g.send(:query_url, {:a => 1, :b => 2})
+    assert_match /^http:/, g.query_url(Geocoder::Query.new("test"))
   end
 end
diff --git a/test/input_handling_test.rb b/test/input_handling_test.rb
deleted file mode 100644
index 9d453b97b562c615bc82178495dba36c05e9d9a7..0000000000000000000000000000000000000000
--- a/test/input_handling_test.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# encoding: utf-8
-require 'test_helper'
-
-class InputHandlingTest < Test::Unit::TestCase
-
-  def test_ip_address_detection
-    assert Geocoder.send(:ip_address?, "232.65.123.94")
-    assert Geocoder.send(:ip_address?, "666.65.123.94") # technically invalid
-    assert !Geocoder.send(:ip_address?, "232.65.123.94.43")
-    assert !Geocoder.send(:ip_address?, "232.65.123")
-  end
-
-  def test_blank_query_detection
-    assert Geocoder.send(:blank_query?, nil)
-    assert Geocoder.send(:blank_query?, "")
-    assert Geocoder.send(:blank_query?, "\t  ")
-    assert !Geocoder.send(:blank_query?, "a")
-    assert !Geocoder.send(:blank_query?, "Москва") # no ASCII characters
-  end
-
-  def test_coordinates_detection
-    lookup = Geocoder::Lookup::Google.new
-    assert lookup.send(:coordinates?, "51.178844,5")
-    assert lookup.send(:coordinates?, "51.178844, -1.826189")
-    assert !lookup.send(:coordinates?, "232.65.123")
-  end
-
-  def test_does_not_choke_on_nil_address
-    all_lookups.each do |l|
-      Geocoder::Configuration.lookup = l
-      assert_nothing_raised { Venue.new("Venue", nil).geocode }
-    end
-  end
-
-  def test_extract_coordinates
-    coords = [-23,47]
-    l = Landmark.new("Madagascar", coords[0], coords[1])
-    assert_equal coords, Geocoder::Calculations.extract_coordinates(l)
-    assert_equal coords, Geocoder::Calculations.extract_coordinates(coords)
-  end
-end
diff --git a/test/integration/smoke_test.rb b/test/integration/smoke_test.rb
index 8d80e1e14730cd9ad53072ef2ff9c07c5f4cf373..0b7cf2fbabe3c9cd31cab65ddcd135ca5e62b24b 100644
--- a/test/integration/smoke_test.rb
+++ b/test/integration/smoke_test.rb
@@ -8,17 +8,19 @@ class SmokeTest < Test::Unit::TestCase
 
   def test_simple_zip_code_search
     result = Geocoder.search "27701"
-    assert_equal "Durham", result.first.city
-    assert_equal "North Carolina", result.first.state
+    assert_not_nil (r = result.first)
+    assert_equal "Durham", r.city
+    assert_equal "North Carolina", r.state
   end
 
   def test_simple_zip_code_search_with_ssl
-    Geocoder::Configuration.use_https = true
+    Geocoder.configure(:use_https => true)
     result = Geocoder.search "27701"
-    assert_equal "Durham", result.first.city
-    assert_equal "North Carolina", result.first.state
+    assert_not_nil (r = result.first)
+    assert_equal "Durham", r.city
+    assert_equal "North Carolina", r.state
   ensure
-    Geocoder::Configuration.use_https = false
+    Geocoder.configure(:use_https => false)
   end
 
 end
diff --git a/test/lookup_test.rb b/test/lookup_test.rb
index 3c72d05b84294fce0a7ce0394ee9645d649edf81..51c2e0d600ccf58a52ff579a1b4cce277c069b72 100644
--- a/test/lookup_test.rb
+++ b/test/lookup_test.rb
@@ -3,28 +3,114 @@ require 'test_helper'
 
 class LookupTest < Test::Unit::TestCase
 
+  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
-    all_lookups.each do |l|
-      lookup = Geocoder.send(:get_lookup, l)
-      assert_equal [], lookup.send(:results, "no results"),
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      lookup = Geocoder::Lookup.get(l)
+      set_api_key!(l)
+      assert_equal [], lookup.send(:results, Geocoder::Query.new("no results")),
         "Lookup #{l} does not return empty array when no results."
     end
   end
 
+  def test_query_url_contains_values_in_params_hash
+    Geocoder::Lookup.all_services_except_test.each do |l|
+      next if l == :freegeoip # 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"}
+      ))
+      # should be "+"s for all lookups except Yahoo
+      assert_match /one_in_the_hand=two(%20|\+)in(%20|\+)the(%20|\+)bush/, url,
+        "Lookup #{l} does not appear to support arbitrary params in URL"
+    end
+  end
+
+  {
+    :bing => :key,
+    :geocoder_ca => :auth,
+    :google => :language,
+    :google_premier => :language,
+    :mapquest => :key,
+    :maxmind => :l,
+    :nominatim => :"accept-language",
+    :yahoo => :locale,
+    :yandex => :plng
+  }.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
+
+  def test_raises_exception_on_invalid_key
+    Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    #Geocoder::Lookup.all_services_except_test.each do |l|
+    [:bing, :yahoo, :yandex, :maxmind].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
+    # keep test output clean: suppress timeout warning
+    orig = $VERBOSE; $VERBOSE = nil
+    #Geocoder::Lookup.all_services_except_test.each do |l|
+    [:bing, :yahoo, :yandex, :maxmind].each do |l|
+      Geocoder.configure(:lookup => l)
+      set_api_key!(l)
+      assert_equal [], Geocoder.search("invalid key")
+    end
+  ensure
+    $VERBOSE = orig
+  end
+
+  def test_does_not_choke_on_nil_address
+    Geocoder::Lookup.all_services.each do |l|
+      Geocoder.configure(:lookup => l)
+      assert_nothing_raised { Venue.new("Venue", 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_google_api_key
-    Geocoder::Configuration.api_key = "MY_KEY"
+    Geocoder.configure(:api_key => "MY_KEY")
     g = Geocoder::Lookup::Google.new
-    assert_match "key=MY_KEY", g.send(:query_url, "Madison Square Garden, New York, NY  10001, United States")
+    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_raises_configuration_error_on_missing_key
+    assert_raises Geocoder::ConfigurationError do
+      Geocoder.configure(:lookup => :bing, :api_key => nil)
+      Geocoder.search("Madison Square Garden, New York, NY  10001, United States")
+    end
   end
 
-  def test_yahoo_app_id
-    Geocoder::Configuration.api_key = "MY_KEY"
-    g = Geocoder::Lookup::Yahoo.new
-    assert_match "appid=MY_KEY", g.send(:query_url, "Madison Square Garden, New York, NY  10001, United States")
+  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/mongoid_test.rb b/test/mongoid_test.rb
index 44b4ca9a0798538974177cfb79332657d9908e3b..1af5e9cd56e64d617b0e0fcee676de9b14925e16 100644
--- a/test/mongoid_test.rb
+++ b/test/mongoid_test.rb
@@ -1,12 +1,7 @@
 # encoding: utf-8
-require 'test_helper'
-
-begin
-require 'mongoid'
 require 'mongoid_test_helper'
 
 class MongoidTest < Test::Unit::TestCase
-
   def test_geocoded_check
     p = Place.new(*venue_params(:msg))
     p.location = [40.750354, -73.993371]
@@ -22,10 +17,23 @@ class MongoidTest < Test::Unit::TestCase
   def test_custom_coordinate_field_near_scope
     location = [40.750354, -73.993371]
     p = Place.near(location)
-    assert_equal p.selector[:location]['$nearSphere'], location.reverse
+    key = Mongoid::VERSION >= "3" ? "location" : :location
+    assert_equal p.selector[key]['$nearSphere'], location.reverse
   end
-end
 
-rescue LoadError => crash
-  warn 'Mongoid not installed, not tested.'
+  def test_model_configuration
+    p = Place.new(*venue_params(:msg))
+    p.location = [0, 0]
+
+    Place.geocoded_by :address, :coordinates => :location, :units => :km
+    assert_equal 111, p.distance_to([0,1]).round
+
+    Place.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
+    result = PlaceWithoutIndex.index_options.keys.flatten[0] == :coordinates
+    assert !result
+  end
 end
diff --git a/test/mongoid_test_helper.rb b/test/mongoid_test_helper.rb
index 69eb2fd044c593898780e3e73fb2f8463905e207..cf37bb01c236fb9b6df27b81f39bcbed5884e1a2 100644
--- a/test/mongoid_test_helper.rb
+++ b/test/mongoid_test_helper.rb
@@ -1,11 +1,18 @@
 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'))
 
-Mongoid.configure do |config|
-  config.logger = Logger.new($stderr, :debug)
+if (::Mongoid::VERSION >= "3")
+  Mongoid.logger = Logger.new($stderr, :debug)
+else
+  Mongoid.configure do |config|
+    config.logger = Logger.new($stderr, :debug)
+  end
 end
 
 ##
@@ -26,3 +33,11 @@ class Place
     write_attribute :address, address
   end
 end
+
+class PlaceWithoutIndex
+  include Mongoid::Document
+  include Geocoder::Model::Mongoid
+
+  field :location, :type => Array
+  geocoded_by :location, :skip_index => true
+end
diff --git a/test/near_test.rb b/test/near_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..05411dd555f95b4e5e20f06cfcd86363fb3ddd95
--- /dev/null
+++ b/test/near_test.rb
@@ -0,0 +1,43 @@
+require 'test_helper'
+
+class NearTest < Test::Unit::TestCase
+
+  def test_near_scope_options_without_sqlite_includes_bounding_box_condition
+    result = Event.send(:near_scope_options, 1.0, 2.0, 5)
+
+    assert_match /test_table_name.latitude BETWEEN 0.9276\d* AND 1.0723\d* AND test_table_name.longitude BETWEEN 1.9276\d* AND 2.0723\d* AND /,
+      result[:conditions][0]
+  end
+
+  def test_near_scope_options_with_defaults
+    result = Event.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 = Event.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 = Event.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
+
+  private
+
+  def assert_no_consecutive_comma(string)
+    assert_no_match /, *,/, string, "two consecutive commas"
+  end
+end
diff --git a/test/oauth_util_test.rb b/test/oauth_util_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f9d1b53478c10729780e7723f2d526d522d413bd
--- /dev/null
+++ b/test/oauth_util_test.rb
@@ -0,0 +1,30 @@
+# encoding: utf-8
+require 'test_helper'
+require 'cgi'
+require 'uri'
+
+class OauthUtilTest < Test::Unit::TestCase
+  def test_query_string_escapes_single_quote
+    base_url = "http://example.com?location=d'iberville"
+
+    o = OauthUtil.new
+    o.consumer_key = 'consumer_key'
+    o.consumer_secret = 'consumer_secret'
+
+    query_string = o.sign(URI.parse(base_url)).query_string
+
+    assert_match "location=d%27iberville", query_string
+  end
+
+  def test_query_string_sorts_url_keys
+    base_url = "http://example.com?a_param=a&z_param=b&b_param=c&n_param=d"
+
+    o = OauthUtil.new
+    o.consumer_key = 'consumer_key'
+    o.consumer_secret = 'consumer_secret'
+
+    query_string = o.sign(URI.parse(base_url)).query_string
+
+    assert_match /.*a_param=.*b_param=.*n_param=.*z_param=.*/, query_string
+  end
+end
diff --git a/test/proxy_test.rb b/test/proxy_test.rb
index d701fdce39d2663d5732379676d6960e2268c796..654b8160c0dd8c89fb3affbda21a8bebc423aedd 100644
--- a/test/proxy_test.rb
+++ b/test/proxy_test.rb
@@ -4,7 +4,7 @@ require 'test_helper'
 class ProxyTest < Test::Unit::TestCase
 
   def test_uses_proxy_when_specified
-    Geocoder::Configuration.http_proxy = 'localhost'
+    Geocoder.configure(:http_proxy => 'localhost')
     lookup = Geocoder::Lookup::Google.new
     assert lookup.send(:http_client).proxy_class?
   end
@@ -15,7 +15,7 @@ class ProxyTest < Test::Unit::TestCase
   end
 
   def test_exception_raised_on_bad_proxy_url
-    Geocoder::Configuration.http_proxy = ' \\_O< Quack Quack'
+    Geocoder.configure(:http_proxy => ' \\_O< Quack Quack')
     assert_raise Geocoder::ConfigurationError do
       Geocoder::Lookup::Google.new.send(:http_client)
     end
diff --git a/test/query_test.rb b/test/query_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..52da81fc03c98bc8fe66899fc7f2ca1a32be08a5
--- /dev/null
+++ b/test/query_test.rb
@@ -0,0 +1,42 @@
+# encoding: utf-8
+require 'test_helper'
+
+class QueryTest < Test::Unit::TestCase
+
+  def test_ip_address_detection
+    assert Geocoder::Query.new("232.65.123.94").ip_address?
+    assert Geocoder::Query.new("666.65.123.94").ip_address? # technically invalid
+    assert Geocoder::Query.new("::ffff:12.34.56.78").ip_address?
+    assert !Geocoder::Query.new("232.65.123.94.43").ip_address?
+    assert !Geocoder::Query.new("232.65.123").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(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?
+  end
+
+  def test_loopback_ip_address
+    assert Geocoder::Query.new("0.0.0.0").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
+end
diff --git a/test/request_test.rb b/test/request_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3acbe2841a0d9bc307eeb0a0ec3e301cfe70e901
--- /dev/null
+++ b/test/request_test.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+require 'test_helper'
+
+class RequestTest < Test::Unit::TestCase
+  class MockRequest
+    include Geocoder::Request
+    attr_accessor :env, :ip
+    def initialize(env={}, ip="")
+      @env = env
+      @ip  = ip
+    end
+  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_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.59"})
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+  def test_with_request_ip
+    req = MockRequest.new({}, "74.200.247.59")
+    assert req.location.is_a?(Geocoder::Result::Freegeoip)
+  end
+end
\ No newline at end of file
diff --git a/test/services_test.rb b/test/services_test.rb
index 83fe571a0eaaa21a066339da4857ddf9cfedc32b..c2eb493f88cd0c2a7019a8a784a7e59bcc6b4968 100644
--- a/test/services_test.rb
+++ b/test/services_test.rb
@@ -3,7 +3,6 @@ require 'test_helper'
 
 class ServicesTest < Test::Unit::TestCase
 
-
   # --- Google ---
 
   def test_google_result_components
@@ -12,6 +11,18 @@ class ServicesTest < Test::Unit::TestCase
       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
@@ -22,60 +33,117 @@ class ServicesTest < Test::Unit::TestCase
     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_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
 
   # --- Google Premier ---
 
   def test_google_premier_result_components
-    Geocoder::Configuration.lookup = :google_premier
+    Geocoder.configure(:lookup => :google_premier)
+    set_api_key!(:google_premier)
     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_premier_query_url
-    Geocoder::Configuration.api_key = ["deadbeef", "gme-test", "test-dev"]
+    Geocoder.configure(:api_key => ["deadbeef", "gme-test", "test-dev"])
     assert_equal "http://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=",
-      Geocoder::Lookup::GooglePremier.new.send(:query_url, "Madison Square Garden, New York, NY", false)
+      Geocoder::Lookup::GooglePremier.new.query_url(Geocoder::Query.new("Madison Square Garden, New York, NY"))
   end
 
 
   # --- Yahoo ---
 
+  def test_yahoo_no_results
+    Geocoder.configure(:lookup => :yahoo)
+    set_api_key!(:yahoo)
+    assert_equal [], Geocoder.search("no results")
+  end
+
+  def test_yahoo_error
+    Geocoder.configure(:lookup => :yahoo)
+    set_api_key!(:yahoo)
+    # keep test output clean: suppress timeout warning
+    orig = $VERBOSE; $VERBOSE = nil
+    assert_equal [], Geocoder.search("error")
+  ensure
+    $VERBOSE = orig
+  end
+
   def test_yahoo_result_components
-    Geocoder::Configuration.lookup = :yahoo
-    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    Geocoder.configure(:lookup => :yahoo)
+    set_api_key!(:yahoo)
+    result = Geocoder.search("madison square garden").first
     assert_equal "10001", result.postal_code
   end
 
   def test_yahoo_address_formatting
-    Geocoder::Configuration.lookup = :yahoo
-    result = Geocoder.search("Madison Square Garden, New York, NY").first
-    assert_equal "Madison Square Garden, New York, NY  10001, United States",
-      result.address
+    Geocoder.configure(:lookup => :yahoo)
+    set_api_key!(:yahoo)
+    result = Geocoder.search("madison square garden").first
+    assert_equal "Madison Square Garden, New York, NY 10001, United States", result.address
   end
 
-
-  # --- Yandex ---
-
-  def test_yandex_with_invalid_key
-    # keep test output clean: suppress timeout warning
-    orig = $VERBOSE; $VERBOSE = nil
-    Geocoder::Configuration.lookup = :yandex
-    assert_equal [], Geocoder.search("invalid key")
-    $VERBOSE = orig
+  def test_yahoo_raises_exception_when_over_query_limit
+    Geocoder.configure(:always_raise => [Geocoder::OverQueryLimitError])
+    l = Geocoder::Lookup.get(:yahoo)
+    assert_raises Geocoder::OverQueryLimitError do
+      l.send(:results, Geocoder::Query.new("over limit"))
+    end
   end
 
-
   # --- Geocoder.ca ---
 
   def test_geocoder_ca_result_components
-    Geocoder::Configuration.lookup = :geocoder_ca
+    Geocoder.configure(:lookup => :geocoder_ca)
+    set_api_key!(:geocoder_ca)
     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
@@ -94,11 +162,56 @@ class ServicesTest < Test::Unit::TestCase
     assert_equal "Plano, TX 75093, United States", result.address
   end
 
+  # --- MaxMind ---
+
+  def test_maxmind_result_on_ip_address_search
+    Geocoder.configure(:ip_lookup => :maxmind, :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(:ip_lookup => :maxmind)
+    assert_equal :country, Geocoder.search("24.24.24.21").first.service_name
+  end
+
+  def test_maxmind_result_knows_city_service_name
+    Geocoder.configure(:ip_lookup => :maxmind)
+    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(:ip_lookup => :maxmind)
+    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(:ip_lookup => :maxmind)
+    assert_equal :omni, Geocoder.search("24.24.24.24").first.service_name
+  end
+
+  def test_maxmind_special_result_components
+    Geocoder.configure(:ip_lookup => :maxmind)
+    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(:ip_lookup => :maxmind)
+    Geocoder.configure(:maxmind => {:service => nil})
+    assert_raises Geocoder::ConfigurationError do
+      Geocoder::Query.new("24.24.24.24").url
+    end
+  end
+
 
   # --- Bing ---
 
   def test_bing_result_components
-    Geocoder::Configuration.lookup = :bing
+    Geocoder.configure(:lookup => :bing)
+    set_api_key!(:bing)
     result = Geocoder.search("Madison Square Garden, New York, NY").first
     assert_equal "Madison Square Garden, NY", result.address
     assert_equal "NY", result.state
@@ -106,23 +219,59 @@ class ServicesTest < Test::Unit::TestCase
   end
 
   def test_bing_no_results
-    Geocoder::Configuration.lookup = :bing
+    Geocoder.configure(:lookup => :bing)
+    set_api_key!(:bing)
     results = Geocoder.search("no results")
     assert_equal 0, results.length
   end
 
   # --- Nominatim ---
 
-   def test_nominatim_result_components
-    Geocoder::Configuration.lookup = :nominatim
+  def test_nominatim_result_components
+    Geocoder.configure(:lookup => :nominatim)
+    set_api_key!(:nominatim)
     result = Geocoder.search("Madison Square Garden, New York, NY").first
     assert_equal "10001", result.postal_code
   end
 
   def test_nominatim_address_formatting
-    Geocoder::Configuration.lookup = :nominatim
+    Geocoder.configure(:lookup => :nominatim)
+    set_api_key!(:nominatim)
     result = Geocoder.search("Madison Square Garden, New York, NY").first
     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_nominatim_host_config
+    Geocoder.configure(:lookup => :nominatim, :nominatim => {:host => "local.com"})
+    lookup = Geocoder::Lookup::Nominatim.new
+    query = Geocoder::Query.new("Bluffton, SC")
+    assert_match %r(http://local\.com), lookup.query_url(query)
+  end
+
+  # --- MapQuest ---
+
+  def test_api_route
+    Geocoder.configure(:lookup => :mapquest, :api_key => "abc123")
+    lookup = Geocoder::Lookup::Mapquest.new
+    query = Geocoder::Query.new("Bluffton, SC")
+    res = lookup.query_url(query)
+    assert_equal "http://www.mapquestapi.com/geocoding/v1/address?key=abc123&location=Bluffton%2C+SC",
+      res
+  end
+
+  def test_mapquest_result_components
+    Geocoder.configure(:lookup => :mapquest)
+    set_api_key!(:mapquest)
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "10001", result.postal_code
+  end
+
+  def test_mapquest_address_formatting
+    Geocoder.configure(:lookup => :mapquest)
+    set_api_key!(:mapquest)
+    result = Geocoder.search("Madison Square Garden, New York, NY").first
+    assert_equal "46 West 31st Street, New York, NY, 10001, US",
+      result.address
+  end
 end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d34b63c0359256e40432bea7c691d83042dc94ad..b74017d41d0d497d8b61b60ea933e452613cf452 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -4,6 +4,12 @@ require 'test/unit'
 $LOAD_PATH.unshift(File.dirname(__FILE__))
 $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
 
+class MysqlConnection
+  def adapter_name
+    "mysql"
+  end
+end
+
 ##
 # Simulate enough of ActiveRecord::Base that objects can be used for testing.
 #
@@ -28,6 +34,10 @@ module ActiveRecord
 
     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
@@ -35,6 +45,17 @@ module ActiveRecord
         read_attribute name
       end
     end
+
+    class << self
+      def table_name
+        'test_table_name'
+      end
+
+      def primary_key
+        :id
+      end
+    end
+
   end
 end
 
@@ -52,114 +73,70 @@ require "geocoder/lookups/base"
 module Geocoder
   module Lookup
     class Base
-      private #-----------------------------------------------------------------
-      def read_fixture(file)
-        File.read(File.join("test", "fixtures", file)).strip.gsub(/\n\s*/, "")
+      private
+      def fixture_exists?(filename)
+        File.exist?(File.join("test", "fixtures", filename))
       end
-    end
 
-    class Google < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        file = case query
-          when "no results";   :no_results
-          when "no locality";  :no_locality
-          when "no city data"; :no_city_data
-          else                 :madison_square_garden
+      def read_fixture(file)
+        filepath = File.join("test", "fixtures", file)
+        s = File.read(filepath).strip.gsub(/\n\s*/, "")
+        s.instance_eval do
+          def body; self; end
+          def code; "200"; end
         end
-        read_fixture "google_#{file}.json"
+        s
       end
-    end
 
-    class GooglePremier < Google
-    end
+      ##
+      # Fixture to use if none match the given query.
+      #
+      def default_fixture_filename
+        "#{fixture_prefix}_madison_square_garden"
+      end
 
-    class Yahoo < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        file = case query
-          when "no results";  :no_results
-          else                :madison_square_garden
-        end
-        read_fixture "yahoo_#{file}.json"
+      def fixture_prefix
+        handle
       end
-    end
 
-    class Yandex < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        file = case query
-          when "no results";  :no_results
-          when "invalid key"; :invalid_key
-          else                :kremlin
-        end
-        read_fixture "yandex_#{file}.json"
+      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
+
+      def make_api_request(query)
+        raise TimeoutError if query.text == "timeout"
+        raise SocketError if query.text == "socket_error"
+        read_fixture fixture_for_query(query)
       end
     end
 
-    class GeocoderCa < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        if reverse
-          read_fixture "geocoder_ca_reverse.json"
-        else
-          file = case query
-            when "no results";  :no_results
-            else                :madison_square_garden
-          end
-          read_fixture "geocoder_ca_#{file}.json"
-        end
+    class GooglePremier
+      private
+      def fixture_prefix
+        "google"
       end
     end
 
-    class Freegeoip < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        file = case query
-          when "no results";  :no_results
-          else                "74_200_247_59"
-        end
-        read_fixture "freegeoip_#{file}.json"
+    class Yandex
+      private
+      def default_fixture_filename
+        "yandex_kremlin"
       end
     end
 
-    class Bing < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        if reverse
-          read_fixture "bing_reverse.json"
-        else
-          file = case query
-            when "no results";  :no_results
-            else                :madison_square_garden
-          end
-          read_fixture "bing_#{file}.json"
-        end
+    class Freegeoip
+      private
+      def default_fixture_filename
+        "freegeoip_74_200_247_59"
       end
     end
 
-  class Nominatim < Base
-      private #-----------------------------------------------------------------
-      def fetch_raw_data(query, reverse = false)
-        raise TimeoutError if query == "timeout"
-        raise SocketError if query == "socket_error"
-        file = case query
-          when "no results";  :no_results
-          else                :madison_square_garden
-        end
-        read_fixture "nominatim_#{file}.json"
+    class Maxmind
+      private
+      def default_fixture_filename
+        "maxmind_74_200_247_59"
       end
     end
 
@@ -179,6 +156,20 @@ class Venue < ActiveRecord::Base
   end
 end
 
+##
+# Geocoded model.
+# - Has user-defined primary key (not just 'id')
+#
+class VenuePlus < Venue
+
+  class << self
+    def primary_key
+      :custom_primary_key_id
+    end
+  end
+
+end
+
 ##
 # Reverse geocoded model.
 #
@@ -247,6 +238,10 @@ end
 
 class Test::Unit::TestCase
 
+  def setup
+    Geocoder.configure(:maxmind => {:service => :city_isp_org})
+  end
+
   def teardown
     Geocoder.send(:remove_const, :Configuration)
     load "geocoder/configuration.rb"
@@ -264,11 +259,21 @@ class Test::Unit::TestCase
     }[abbrev]
   end
 
-  def all_lookups
-    Geocoder.valid_lookups
+  def is_nan_coordinates?(coordinates)
+    return false unless coordinates.respond_to? :size # Should be an array
+    return false unless coordinates.size == 2 # Should have dimension 2
+    coordinates[0].nan? && coordinates[1].nan? # Both coordinates should be NaN
   end
 
-  def street_lookups
-    all_lookups - [:freegeoip]
+  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
diff --git a/test/test_mode_test.rb b/test/test_mode_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e80726a29f32fd81db741af141ecdf1b7569e40
--- /dev/null
+++ b/test/test_mode_test.rb
@@ -0,0 +1,50 @@
+require 'test_helper'
+
+class TestModeTest < Test::Unit::TestCase
+
+  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
+    coordinates = [40.7143528, -74.0059731]
+    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',
+    }
+
+    Geocoder::Lookup::Test.add_stub("New York, NY", [attributes])
+
+    results = Geocoder.search("New York, NY")
+    assert_equal 1, results.size
+
+    result = results.first
+    assert_equal coordinates,                result.coordinates
+    assert_equal attributes['latitude'],     result.latitude
+    assert_equal attributes['longitude'],    result.longitude
+    assert_equal attributes['address'],      result.address
+    assert_equal attributes['state'],        result.state
+    assert_equal attributes['state_code'],   result.state_code
+    assert_equal attributes['country'],      result.country
+    assert_equal attributes['country_code'], result.country_code
+  end
+
+  def test_search_with_unknown_stub
+    assert_raise ArgumentError do
+      Geocoder.search("New York, NY")
+    end
+  end
+
+end