Repository: geopandas/geopandas

Issue:
expose Mapzen Search geocoding service
GeoPy includes [support](https://github.com/geopy/geopy/pull/183) for geocoding using [Mapzen Search](https://mapzen.com/documentation/search/).

Would be useful to expose this as a provider through `geopandas.geocode`.
ENH: expose all geopy geocoding services
The current behavior of the geocoding (and reverse) functionality in GeoPandas takes a service provider as a string, which is then mapped to a `geopy.geocoders` instance with a hard coded dict. Instead of taking a string (and, for at least some deprecation time period, in addition), we could take a `geopy.geocoders` class as an argument. In addition to providing access to lots of services (in addition to Mapzen, explicitly requested in #468), when `geopy` adds or removes services, we wouldn't need to worry about keeping up. The different services already accept different args/kwargs, and thus the user must already have familiarity with the different services if, e.g., using an API key for the service. I'd suggest still leaving GoogleV3 as the default, so the experience for most users wouldn't change at all. In general, the call would change from

    df = gpd.tools.geocoding.reverse_geocode([Point(-71.0594869, 42.3584697), Point(-77.0365305, 38.8977332)], 
                                              provider='nominatim')

to 

    df = gpd.tools.geocoding.reverse_geocode([Point(-71.0594869, 42.3584697), Point(-77.0365305, 38.8977332)], 
                                              provider=geopy.geocoders.Nominatim)

Anything I'm missing here? If not, happy to implement this change. 



Discussion:
@drewda that shouldn't be difficult to add. Do you want to make a PR for this?
Yes, certainly OK to start allowing the actual class instead of string. 
I *think* I would leave the current strings as is, as it otherwise would break a lot of code and is somewhat convenient, but we can also deprecate.

Resolving patch:
diff --git a/geopandas/tools/geocoding.py b/geopandas/tools/geocoding.py
--- a/geopandas/tools/geocoding.py
+++ b/geopandas/tools/geocoding.py
@@ -5,7 +5,7 @@
 import numpy as np
 import pandas as pd
 from shapely.geometry import Point
-from six import iteritems
+from six import iteritems, string_types
 
 import geopandas as gpd
 
@@ -17,7 +17,8 @@ def _throttle_time(provider):
     require a maximum of 1 request per second.
     https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy
     """
-    if provider == 'nominatim':
+    import geopy.geocoders
+    if provider == geopy.geocoders.Nominatim:
         return 1
     else:
         return 0
@@ -30,15 +31,14 @@ def geocode(strings, provider='googlev3', **kwargs):
     Parameters
     ----------
     strings : list or Series of addresses to geocode
-    provider : geopy geocoder to use, default 'googlev3'
+    provider : str or geopy.geocoder
+        Specifies geocoding service to use, default is 'googlev3'.
+        Either the string name used by geopy (as specified in
+        geopy.geocoders.SERVICE_TO_GEOCODER) or a geopy Geocoder instance
+        (e.g., geopy.geocoders.GoogleV3) may be used.
+
         Some providers require additional arguments such as access keys
         See each geocoder's specific parameters in geopy.geocoders
-        * googlev3, default
-        * bing
-        * google
-        * yahoo
-        * mapquest
-        * openmapquest
 
     Ensure proper use of the results by consulting the Terms of Service for
     your provider.
@@ -74,16 +74,14 @@ def reverse_geocode(points, provider='googlev3', **kwargs):
     points : list or Series of Shapely Point objects.
         x coordinate is longitude
         y coordinate is latitude
-    provider : geopy geocoder to use, default 'googlev3'
-        These are the same options as the geocode() function
+    provider : str or geopy.geocoder (opt)
+        Specifies geocoding service to use, default is 'googlev3'.
+        Either the string name used by geopy (as specified in
+        geopy.geocoders.SERVICE_TO_GEOCODER) or a geopy Geocoder instance
+        (e.g., geopy.geocoders.GoogleV3) may be used.
+
         Some providers require additional arguments such as access keys
         See each geocoder's specific parameters in geopy.geocoders
-        * googlev3, default
-        * bing
-        * google
-        * yahoo
-        * mapquest
-        * openmapquest
 
     Ensure proper use of the results by consulting the Terms of Service for
     your provider.
@@ -109,28 +107,18 @@ def reverse_geocode(points, provider='googlev3', **kwargs):
 
 
 def _query(data, forward, provider, **kwargs):
+    # generic wrapper for calls over lists to geopy Geocoders
     import geopy
     from geopy.geocoders.base import GeocoderQueryError
+    from geopy.geocoders import get_geocoder_for_service
 
     if not isinstance(data, pd.Series):
         data = pd.Series(data)
 
-    # workaround changed name in 0.96
-    try:
-        Yahoo = geopy.geocoders.YahooPlaceFinder
-    except AttributeError:
-        Yahoo = geopy.geocoders.Yahoo
-
-    coders = {'googlev3': geopy.geocoders.GoogleV3,
-              'bing': geopy.geocoders.Bing,
-              'yahoo': Yahoo,
-              'openmapquest': geopy.geocoders.OpenMapQuest,
-              'nominatim': geopy.geocoders.Nominatim}
-
-    if provider not in coders:
-        raise ValueError('Unknown geocoding provider: {0}'.format(provider))
+    if isinstance(provider, string_types):
+        provider = get_geocoder_for_service(provider)
 
-    coder = coders[provider](**kwargs)
+    coder = provider(**kwargs)
     results = {}
     for i, s in iteritems(data):
         try:


Test patch:
diff --git a/geopandas/tests/test_geocode.py b/geopandas/tests/test_geocode.py
--- a/geopandas/tests/test_geocode.py
+++ b/geopandas/tests/test_geocode.py
@@ -105,41 +105,47 @@ def test_prepare_result_none(self):
         row = df.loc['b']
         assert len(row['geometry'].coords) == 0
         assert np.isnan(row['address'])
-    
+
     def test_bad_provider_forward(self):
-        with pytest.raises(ValueError):
+        from geopy.exc import GeocoderNotFound
+        with pytest.raises(GeocoderNotFound):
             geocode(['cambridge, ma'], 'badprovider')
 
     def test_bad_provider_reverse(self):
-        with pytest.raises(ValueError):
+        from geopy.exc import GeocoderNotFound
+        with pytest.raises(GeocoderNotFound):
             reverse_geocode(['cambridge, ma'], 'badprovider')
 
     def test_forward(self):
-        with mock.patch('geopy.geocoders.googlev3.GoogleV3.geocode',
-                        ForwardMock()) as m:
-            g = geocode(self.locations, provider='googlev3', timeout=2)
-            assert len(self.locations) == m.call_count
-
-        n = len(self.locations)
-        assert isinstance(g, GeoDataFrame)
-        expected = GeoSeries(
-            [Point(float(x) + 0.5, float(x)) for x in range(n)],
-            crs=from_epsg(4326))
-        assert_geoseries_equal(expected, g['geometry'])
-        assert_series_equal(g['address'],
-                            pd.Series(self.locations, name='address'))
+        from geopy.geocoders import GoogleV3
+        for provider in ['googlev3', GoogleV3]:
+            with mock.patch('geopy.geocoders.googlev3.GoogleV3.geocode',
+                            ForwardMock()) as m:
+                g = geocode(self.locations, provider=provider, timeout=2)
+                assert len(self.locations) == m.call_count
+
+            n = len(self.locations)
+            assert isinstance(g, GeoDataFrame)
+            expected = GeoSeries(
+                [Point(float(x) + 0.5, float(x)) for x in range(n)],
+                crs=from_epsg(4326))
+            assert_geoseries_equal(expected, g['geometry'])
+            assert_series_equal(g['address'],
+                                pd.Series(self.locations, name='address'))
 
     def test_reverse(self):
-        with mock.patch('geopy.geocoders.googlev3.GoogleV3.reverse',
-                        ReverseMock()) as m:
-            g = reverse_geocode(self.points, provider='googlev3', timeout=2)
-            assert len(self.points) == m.call_count
-
-        assert isinstance(g, GeoDataFrame)
-
-        expected = GeoSeries(self.points, crs=from_epsg(4326))
-        assert_geoseries_equal(expected, g['geometry'])
-        address = pd.Series(
-            ['address' + str(x) for x in range(len(self.points))],
-            name='address')
-        assert_series_equal(g['address'], address)
+        from geopy.geocoders import GoogleV3
+        for provider in ['googlev3', GoogleV3]:
+            with mock.patch('geopy.geocoders.googlev3.GoogleV3.reverse',
+                            ReverseMock()) as m:
+                g = reverse_geocode(self.points, provider=provider, timeout=2)
+                assert len(self.points) == m.call_count
+
+            assert isinstance(g, GeoDataFrame)
+
+            expected = GeoSeries(self.points, crs=from_epsg(4326))
+            assert_geoseries_equal(expected, g['geometry'])
+            address = pd.Series(
+                ['address' + str(x) for x in range(len(self.points))],
+                name='address')
+            assert_series_equal(g['address'], address)