diff --git a/README.md b/README.md
index c35c2f39275439a757cea636f33fabf5148d6e9f..26c76d5f158296fcda48ad572cf1953183dd197a 100644
--- a/README.md
+++ b/README.md
@@ -394,6 +394,17 @@ Yahoo BOSS is **not a free service**. As of November 17, 2012 Yahoo no longer of
 * **Terms of Service**: ?
 * **Limitations**: ?
 
+#### MaxMind Web Services (`:maxmind`)
+
+* **API key**: required
+* **Quota**: Request Packs can be purchased
+* **Region**: world
+* **SSL support**: no
+* **Languages**: English
+* **Documentation**: http://www.maxmind.com/app/web_services
+* **Terms of Service**: ?
+* **Limitations**: ?
+
 
 Caching
 -------
diff --git a/lib/geocoder/exceptions.rb b/lib/geocoder/exceptions.rb
index 0ed0eb0d3954d0d2b6457655aff9f66a0c1b3250..7445c662dc7aa548cc065c69452fa8f3573e0bd9 100644
--- a/lib/geocoder/exceptions.rb
+++ b/lib/geocoder/exceptions.rb
@@ -15,4 +15,7 @@ module Geocoder
   class InvalidRequest < Error
   end
 
+  class InvalidApiKey < Error
+  end
+
 end
diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb
index 4e58888d09ab9f46a4e5287883a4a23075f92e4e..472a164a01193e823eac1cea726dfae4f685f074 100644
--- a/lib/geocoder/lookup.rb
+++ b/lib/geocoder/lookup.rb
@@ -37,7 +37,7 @@ module Geocoder
     # All IP address lookup services, default first.
     #
     def ip_services
-      [:freegeoip]
+      [:freegeoip, :maxmind]
     end
 
     ##
diff --git a/lib/geocoder/lookups/maxmind.rb b/lib/geocoder/lookups/maxmind.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9ca10504ad317d9b48d48987687a09be7168cea8
--- /dev/null
+++ b/lib/geocoder/lookups/maxmind.rb
@@ -0,0 +1,50 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/maxmind'
+require 'csv'
+
+module Geocoder::Lookup
+  class Maxmind < Base
+
+    def name
+      "MaxMind"
+    end
+
+    private # ---------------------------------------------------------------
+
+    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 doc.size == 10
+          return [doc]
+        elsif doc.size > 10 and doc[10] == "INVALID_LICENSE_KEY"
+          raise_error(Geocoder::InvalidApiKey) ||
+            warn("Invalid MaxMind API key.")
+        end
+      end
+      return []
+    end
+
+    def parse_raw_data(raw_data)
+      # Maxmind just returns text/plain as csv format but according to documentation,
+      # we get ISO-8859-1 encoded string. We need to convert it.
+      CSV.parse_line raw_data.force_encoding("ISO-8859-1").encode("UTF-8")
+    end
+
+    def reserved_result
+      ",,,,0,0,0,0,,"
+    end
+
+    def query_url_params(query)
+      super.merge(
+        :l => configuration.api_key,
+        :i => query.sanitized_text
+      )
+    end
+
+    def query_url(query)
+      "#{protocol}://geoip3.maxmind.com/f?" + url_query_string(query)
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/yahoo.rb b/lib/geocoder/lookups/yahoo.rb
index 43138b62d3ae06879126dd11fd5442871b387f60..77a658b4f3e0a05961eecce51d5e244cdf88036a 100644
--- a/lib/geocoder/lookups/yahoo.rb
+++ b/lib/geocoder/lookups/yahoo.rb
@@ -34,6 +34,23 @@ module Geocoder::Lookup
       end
     end
 
+    ##
+    # 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)
       super.merge(
         :location => query.sanitized_text,
diff --git a/lib/geocoder/lookups/yandex.rb b/lib/geocoder/lookups/yandex.rb
index 4b306e1ef68d0eb596283c43083a60aec5ffda74..272ecea9725e3da552111841dbe97971b40bfba7 100644
--- a/lib/geocoder/lookups/yandex.rb
+++ b/lib/geocoder/lookups/yandex.rb
@@ -21,7 +21,11 @@ module Geocoder::Lookup
     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']
diff --git a/lib/geocoder/results/maxmind.rb b/lib/geocoder/results/maxmind.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8963ab5c729ab4d0a13ed24545e8a7211c5570c0
--- /dev/null
+++ b/lib/geocoder/results/maxmind.rb
@@ -0,0 +1,55 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Maxmind < Base
+
+    def address(format = :full)
+      s = state_code.to_s == "" ? "" : ", #{state_code}"
+      "#{city}#{s} #{postal_code}, #{country_code}".sub(/^[ ,]*/, "")
+    end
+
+    def country_code
+      @data[0]
+    end
+
+    def state_code
+      @data[1]
+    end
+
+    def city
+      @data[2]
+    end
+
+    def postal_code
+      @data[3]
+    end
+
+    def coordinates
+      [@data[4].to_f, @data[5].to_f]
+    end
+
+    def metrocode
+      @data[6]
+    end
+
+    def area_code
+      @data[7]
+    end
+
+    def isp
+      @data[8][1,@data[8].length-2]
+    end
+
+    def organization
+      @data[9][1,@data[9].length-2]
+    end
+
+    def country #not given by MaxMind
+      country_code
+    end
+
+    def state #not given by MaxMind
+      state_code
+    end
+  end
+end
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.json b/test/fixtures/mapquest_madison_square_garden
similarity index 100%
rename from test/fixtures/mapquest_madison_square_garden.json
rename to test/fixtures/mapquest_madison_square_garden
diff --git a/test/fixtures/mapquest_no_results.json b/test/fixtures/mapquest_no_results
similarity index 100%
rename from test/fixtures/mapquest_no_results.json
rename to test/fixtures/mapquest_no_results
diff --git a/test/fixtures/maxmind_74_200_247_59 b/test/fixtures/maxmind_74_200_247_59
new file mode 100644
index 0000000000000000000000000000000000000000..22fce3390f59c095a1c844430478794ceac813d8
--- /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"
\ No newline at end of file
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.json b/test/fixtures/yahoo_error
similarity index 100%
rename from test/fixtures/yahoo_error.json
rename to test/fixtures/yahoo_error
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.json b/test/fixtures/yahoo_madison_square_garden
similarity index 100%
rename from test/fixtures/yahoo_madison_square_garden.json
rename to test/fixtures/yahoo_madison_square_garden
diff --git a/test/fixtures/yahoo_no_results.json b/test/fixtures/yahoo_no_results
similarity index 100%
rename from test/fixtures/yahoo_no_results.json
rename to test/fixtures/yahoo_no_results
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/lookup_test.rb b/test/lookup_test.rb
index d4b9fd34ea8b109f1b9754e36c621c2f93fc46b8..6e77e4ba7d0c2b878f129e6748aab46270a2bb5c 100644
--- a/test/lookup_test.rb
+++ b/test/lookup_test.rb
@@ -3,6 +3,14 @@ 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
     Geocoder::Lookup.all_services_except_test.each do |l|
       lookup = Geocoder::Lookup.get(l)
diff --git a/test/services_test.rb b/test/services_test.rb
index 20a4f403f8e7ac4c1d2cf968b357855d420465b4..9c232a8593f3155658a77f3aa70bf023a43c315e 100644
--- a/test/services_test.rb
+++ b/test/services_test.rb
@@ -105,10 +105,25 @@ class ServicesTest < Test::Unit::TestCase
     assert_equal "Madison Square Garden, New York, NY 10001, United States", result.address
   end
 
+  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
+
+  def test_yahoo_raises_exception_on_invalid_key
+    Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    l = Geocoder::Lookup.get(:yahoo)
+    assert_raises Geocoder::InvalidApiKey do
+      l.send(:results, Geocoder::Query.new("invalid key"))
+    end
+  end
 
   # --- Yandex ---
 
-  def test_yandex_with_invalid_key
+  def test_yandex_warns_about_invalid_key
     # keep test output clean: suppress timeout warning
     orig = $VERBOSE; $VERBOSE = nil
     Geocoder.configure(:lookup => :yandex)
@@ -118,6 +133,14 @@ class ServicesTest < Test::Unit::TestCase
     $VERBOSE = orig
   end
 
+  def test_yandex_raises_exception_on_invalid_key
+    Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey])
+    l = Geocoder::Lookup.get(:yandex)
+    assert_raises Geocoder::InvalidApiKey do
+      l.send(:results, Geocoder::Query.new("invalid key"))
+    end
+  end
+
 
   # --- Geocoder.ca ---
 
@@ -142,6 +165,30 @@ 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::Configuration.ip_lookup = :maxmind
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::Maxmind)
+  end
+
+  def test_maxmind_result_components
+    Geocoder::Configuration.ip_lookup = :maxmind
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Plano, TX 75093, US", result.address
+  end
+
+  def test_maxmind_raises_exception_on_invalid_key
+    Geocoder.configure(
+      :always_raise => [Geocoder::InvalidApiKey]
+    )
+    l = Geocoder::Lookup.get(:maxmind)
+    assert_raises Geocoder::InvalidApiKey do
+      l.send(:results, Geocoder::Query.new("invalid key"))
+    end
+  end
+
 
   # --- Bing ---
 
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 5c93eb1ba3da487e2cd0eaca3ed3980340d34ca9..6fca5010f64d82ecd2dfd1cd5d1ee8bbc34d8c70 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -73,7 +73,11 @@ require "geocoder/lookups/base"
 module Geocoder
   module Lookup
     class Base
-      private #-----------------------------------------------------------------
+      private
+      def fixture_exists?(filename)
+        File.exist?(File.join("test", "fixtures", filename))
+      end
+
       def read_fixture(file)
         filepath = File.join("test", "fixtures", file)
         s = File.read(filepath).strip.gsub(/\n\s*/, "")
@@ -83,124 +87,56 @@ module Geocoder
         end
         s
       end
-    end
 
-    class Google < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        file = case query.text
-          when "no results";   :no_results
-          when "no locality";  :no_locality
-          when "no city data"; :no_city_data
-          else                 :madison_square_garden
-        end
-        read_fixture "google_#{file}.json"
+      def default_fixture_filename
+        "#{fixture_prefix}_madison_square_garden"
       end
-    end
 
-    class GooglePremier < Google
-    end
-
-    class Yahoo < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        file = case query.text
-          when "no results"; :no_results
-          when "error";      :error
-          else               :madison_square_garden
-        end
-        read_fixture "yahoo_#{file}.json"
-      end
-    end
-
-    class Yandex < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        file = case query.text
-          when "no results";  :no_results
-          when "invalid key"; :invalid_key
-          else                :kremlin
-        end
-        read_fixture "yandex_#{file}.json"
+      def fixture_prefix
+        handle
       end
-    end
 
-    class GeocoderCa < Base
-      private #-----------------------------------------------------------------
       def make_api_request(query)
         raise TimeoutError if query.text == "timeout"
         raise SocketError if query.text == "socket_error"
         if query.reverse_geocode?
-          read_fixture "geocoder_ca_reverse.json"
+          filename = "#{fixture_prefix}_reverse"
+        else
+          filename = "#{fixture_prefix}_#{query.text.gsub(" ", "_")}"
+        end
+        if fixture_exists?(filename)
+          read_fixture "#{filename}"
         else
-          file = case query.text
-            when "no results";  :no_results
-            else                :madison_square_garden
-          end
-          read_fixture "geocoder_ca_#{file}.json"
+          read_fixture default_fixture_filename
         end
       end
     end
 
-    class Freegeoip < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        file = case query.text
-          when "no results";  :no_results
-          else                "74_200_247_59"
-        end
-        read_fixture "freegeoip_#{file}.json"
+    class GooglePremier
+      private
+      def fixture_prefix
+        "google"
       end
     end
 
-    class Bing < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        if query.reverse_geocode?
-          read_fixture "bing_reverse.json"
-        else
-          file = case query.text
-            when "no results";  :no_results
-            else                :madison_square_garden
-          end
-          read_fixture "bing_#{file}.json"
-        end
+    class Yandex
+      private
+      def default_fixture_filename
+        "yandex_kremlin"
       end
     end
 
-    class Nominatim < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        file = case query.text
-          when "no results";  :no_results
-          else                :madison_square_garden
-        end
-        read_fixture "nominatim_#{file}.json"
+    class Freegeoip
+      private
+      def default_fixture_filename
+        "freegeoip_74_200_247_59"
       end
     end
 
-    class Mapquest < Base
-      private #-----------------------------------------------------------------
-      def make_api_request(query)
-        raise TimeoutError if query.text == "timeout"
-        raise SocketError if query.text == "socket_error"
-        file = case query.text
-          when "no results";  :no_results
-          else                :madison_square_garden
-        end
-        read_fixture "mapquest_#{file}.json"
+    class Maxmind
+      private
+      def default_fixture_filename
+        "maxmind_74_200_247_59"
       end
     end