diff --git a/config/config.exs b/config/config.exs
index 75ba5c4c06a38b35205e0e0e5e9c91750a9992e8..cb708b25ef0ef2a8e307ef80f2f7792b9b867a1f 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -64,3 +64,20 @@ config :arc,
   storage: Arc.Storage.Local
 
 config :phoenix, :format_encoders, json: Jason
+
+config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
+  endpoint:
+    System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org",
+  api_key: System.get_env("GEOSPATIAL_NOMINATIM_API_KEY") || nil
+
+config :mobilizon, Mobilizon.Service.Geospatial.Addok,
+  endpoint: System.get_env("GEOSPATIAL_ADDOK_ENDPOINT") || "https://api-adresse.data.gouv.fr"
+
+config :mobilizon, Mobilizon.Service.Geospatial.Photon,
+  endpoint: System.get_env("GEOSPATIAL_PHOTON_ENDPOINT") || "https://photon.komoot.de"
+
+config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps,
+  api_key: System.get_env("GEOSPATIAL_GOOGLE_MAPS_API_KEY") || nil
+
+config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
+  api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil
diff --git a/config/prod.exs b/config/prod.exs
index 3cea69df62a4aa5b761eda079eb0d9a41d1138eb..969e9553974a4db10c46aee67a8cf1ff2f16cdc8 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -58,6 +58,8 @@ config :mobilizon, Mobilizon.Mailer,
 # Do not print debug messages in production
 config :logger, level: System.get_env("MOBILIZON_LOGLEVEL") |> String.to_atom() || :info
 
+config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
+
 # ## SSL Support
 #
 # To get SSL working, you will need to add the `https` key
diff --git a/config/test.exs b/config/test.exs
index c906f42e8757cffae30ee8b95af3e09bbbbbf859..3339f23a3a5b80e818cdcb27378f7114ab98218e 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -32,3 +32,5 @@ config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter
 
 config :exvcr,
   vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"
+
+config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex
index 891167a31abebd94893b9b2a142a0ab1452b6eea..78607311f3d9ef99bd9fa75c764b9fe467bb12d6 100644
--- a/lib/mobilizon/addresses/addresses.ex
+++ b/lib/mobilizon/addresses/addresses.ex
@@ -108,6 +108,7 @@ defmodule Mobilizon.Addresses do
   @doc """
   Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`.
   """
+  # TODO: Unused, remove me
   def process_geom(%{"type" => type_input, "data" => data}) do
     type =
       if !is_atom(type_input) && type_input != nil do
@@ -145,4 +146,62 @@ defmodule Mobilizon.Addresses do
   defp process_point(_, _) do
     {:error, "Latitude and longitude must be numbers"}
   end
+
+  @doc """
+  Search addresses in our database
+
+  We only look at the description for now, and eventually order by object distance
+  """
+  @spec search_addresses(String.t(), list()) :: list(Address.t())
+  def search_addresses(search, options) do
+    limit = Keyword.get(options, :limit, 5)
+
+    query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit)
+
+    query =
+      if coords = Keyword.get(options, :coords, false),
+        do:
+          from(a in query,
+            order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
+          ),
+        else: query
+
+    query =
+      if country = Keyword.get(options, :country, nil),
+        do: from(a in query, where: ilike(a.addressCountry, ^"%#{country}%")),
+        else: query
+
+    Repo.all(query)
+  end
+
+  @doc """
+  Reverse geocode from coordinates in our database
+
+  We only take addresses 50km around and sort them by distance
+  """
+  @spec reverse_geocode(number(), number(), list()) :: list(Address.t())
+  def reverse_geocode(lon, lat, options) do
+    limit = Keyword.get(options, :limit, 5)
+    radius = Keyword.get(options, :radius, 50_000)
+    country = Keyword.get(options, :country, nil)
+    srid = Keyword.get(options, :srid, 4326)
+
+    import Geo.PostGIS
+
+    with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
+      query =
+        from(a in Address,
+          order_by: [fragment("? <-> ?", a.geom, ^point)],
+          limit: ^limit,
+          where: st_dwithin_in_meters(^point, a.geom, ^radius)
+        )
+
+      query =
+        if country,
+          do: from(a in query, where: ilike(a.addressCountry, ^"%#{country}%")),
+          else: query
+
+      Repo.all(query)
+    end
+  end
 end
diff --git a/lib/mobilizon_web/context.ex b/lib/mobilizon_web/context.ex
index c9abf01d0f8e03bdf04d5647cddb89c03c96b141..be5834d04dc00f59208f83abaf75507c65dda7a7 100644
--- a/lib/mobilizon_web/context.ex
+++ b/lib/mobilizon_web/context.ex
@@ -12,11 +12,17 @@ defmodule MobilizonWeb.Context do
   end
 
   def call(conn, _) do
-    with %User{} = user <- Guardian.Plug.current_resource(conn) do
-      put_private(conn, :absinthe, %{context: %{current_user: user}})
-    else
-      nil ->
-        conn
-    end
+    context = %{ip: to_string(:inet_parse.ntoa(conn.remote_ip))}
+
+    context =
+      case Guardian.Plug.current_resource(conn) do
+        %User{} = user ->
+          Map.put(context, :current_user, user)
+
+        nil ->
+          context
+      end
+
+    put_private(conn, :absinthe, %{context: context})
   end
 end
diff --git a/lib/mobilizon_web/resolvers/address.ex b/lib/mobilizon_web/resolvers/address.ex
new file mode 100644
index 0000000000000000000000000000000000000000..df249365906f5abe9e2e0ba6754ab173866c211c
--- /dev/null
+++ b/lib/mobilizon_web/resolvers/address.ex
@@ -0,0 +1,33 @@
+defmodule MobilizonWeb.Resolvers.Address do
+  @moduledoc """
+  Handles the comment-related GraphQL calls
+  """
+  require Logger
+  alias Mobilizon.Addresses
+  alias Mobilizon.Service.Geospatial
+
+  def search(_parent, %{query: query}, %{context: %{ip: ip}}) do
+    country = Geolix.lookup(ip) |> Map.get(:country, nil)
+
+    local_addresses = Task.async(fn -> Addresses.search_addresses(query, country: country) end)
+
+    remote_addresses = Task.async(fn -> Geospatial.service().search(query) end)
+
+    addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
+
+    {:ok, addresses}
+  end
+
+  def reverse_geocode(_parent, %{longitude: longitude, latitude: latitude}, %{context: %{ip: ip}}) do
+    country = Geolix.lookup(ip) |> Map.get(:country, nil)
+
+    local_addresses =
+      Task.async(fn -> Addresses.reverse_geocode(longitude, latitude, country: country) end)
+
+    remote_addresses = Task.async(fn -> Geospatial.service().geocode(longitude, latitude) end)
+
+    addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
+
+    {:ok, addresses}
+  end
+end
diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex
index 9d140130fd56fbdc5ec9ffa7cdcff8d5edf30ffb..0373b80ce5cb69cae78193d5cb4579b1d33cce20 100644
--- a/lib/mobilizon_web/schema.ex
+++ b/lib/mobilizon_web/schema.ex
@@ -132,6 +132,7 @@ defmodule MobilizonWeb.Schema do
     import_fields(:event_queries)
     import_fields(:participant_queries)
     import_fields(:tag_queries)
+    import_fields(:address_queries)
   end
 
   @desc """
diff --git a/lib/mobilizon_web/schema/address.ex b/lib/mobilizon_web/schema/address.ex
index d3b560a166da8f8f58be648c2aae60821750d5d4..577ead16481823ee01247e74328bce71d0178e17 100644
--- a/lib/mobilizon_web/schema/address.ex
+++ b/lib/mobilizon_web/schema/address.ex
@@ -3,6 +3,7 @@ defmodule MobilizonWeb.Schema.AddressType do
   Schema representation for Address
   """
   use Absinthe.Schema.Notation
+  alias MobilizonWeb.Resolvers
 
   object :physical_address do
     field(:type, :address_type)
@@ -36,4 +37,21 @@ defmodule MobilizonWeb.Schema.AddressType do
     value(:phone, description: "The address is a phone number for a conference")
     value(:other, description: "The address is something else")
   end
+
+  object :address_queries do
+    @desc "Search for an address"
+    field :search_address, type: list_of(:physical_address) do
+      arg(:query, non_null(:string))
+
+      resolve(&Resolvers.Address.search/3)
+    end
+
+    @desc "Reverse geocode coordinates"
+    field :reverse_geocode, type: list_of(:physical_address) do
+      arg(:longitude, non_null(:float))
+      arg(:latitude, non_null(:float))
+
+      resolve(&Resolvers.Address.reverse_geocode/3)
+    end
+  end
 end
diff --git a/lib/service/geospatial/addok.ex b/lib/service/geospatial/addok.ex
new file mode 100644
index 0000000000000000000000000000000000000000..1d0f3f46de042c977e0cc8d0b5a188a381166563
--- /dev/null
+++ b/lib/service/geospatial/addok.ex
@@ -0,0 +1,85 @@
+defmodule Mobilizon.Service.Geospatial.Addok do
+  @moduledoc """
+  [Addok](https://github.com/addok/addok) backend.
+  """
+  alias Mobilizon.Service.Geospatial.Provider
+  require Logger
+  alias Mobilizon.Addresses.Address
+
+  @behaviour Provider
+
+  @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
+
+  @impl Provider
+  @doc """
+  Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
+  """
+  @spec geocode(String.t(), keyword()) :: list(Address.t())
+  def geocode(lon, lat, options \\ []) do
+    url = build_url(:geocode, %{lon: lon, lat: lat}, options)
+
+    Logger.debug("Asking addok for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      processData(features)
+    end
+  end
+
+  @impl Provider
+  @doc """
+  Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
+  """
+  @spec search(String.t(), keyword()) :: list(Address.t())
+  def search(q, options \\ []) do
+    url = build_url(:search, %{q: q}, options)
+    Logger.debug("Asking addok for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      processData(features)
+    end
+  end
+
+  @spec build_url(atom(), map(), list()) :: String.t()
+  defp build_url(method, args, options) do
+    limit = Keyword.get(options, :limit, 10)
+    coords = Keyword.get(options, :coords, nil)
+    endpoint = Keyword.get(options, :endpoint, @endpoint)
+
+    case method do
+      :geocode ->
+        "#{endpoint}/reverse/?lon=#{args.lon}&lat=#{args.lat}&limit=#{limit}"
+
+      :search ->
+        url = "#{endpoint}/search/?q=#{URI.encode(args.q)}&limit=#{limit}"
+        if is_nil(coords), do: url, else: url <> "&lat=#{coords.lat}&lon=#{coords.lon}"
+    end
+  end
+
+  defp processData(features) do
+    features
+    |> Enum.map(fn %{"geometry" => geometry, "properties" => properties} ->
+      %Address{
+        addressCountry: Map.get(properties, "country"),
+        addressLocality: Map.get(properties, "city"),
+        addressRegion: Map.get(properties, "state"),
+        description: Map.get(properties, "name") || streetAddress(properties),
+        floor: Map.get(properties, "floor"),
+        geom: Map.get(geometry, "coordinates") |> Provider.coordinates(),
+        postalCode: Map.get(properties, "postcode"),
+        streetAddress: properties |> streetAddress()
+      }
+    end)
+  end
+
+  defp streetAddress(properties) do
+    if Map.has_key?(properties, "housenumber") do
+      Map.get(properties, "housenumber") <> " " <> Map.get(properties, "street")
+    else
+      Map.get(properties, "street")
+    end
+  end
+end
diff --git a/lib/service/geospatial/geospatial.ex b/lib/service/geospatial/geospatial.ex
new file mode 100644
index 0000000000000000000000000000000000000000..0321997afbc7d593f7881183479e96929d06bfdc
--- /dev/null
+++ b/lib/service/geospatial/geospatial.ex
@@ -0,0 +1,15 @@
+defmodule Mobilizon.Service.Geospatial do
+  @moduledoc """
+  Module to load the service adapter defined inside the configuration
+
+  See `Mobilizon.Service.Geospatial.Provider`
+  """
+
+  @doc """
+  Returns the appropriate service adapter
+
+  According to the config behind `config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Module`
+  """
+  @spec service() :: module()
+  def service(), do: Application.get_env(:mobilizon, __MODULE__) |> get_in([:service])
+end
diff --git a/lib/service/geospatial/google_maps.ex b/lib/service/geospatial/google_maps.ex
new file mode 100644
index 0000000000000000000000000000000000000000..becb15773587a2bb01512ba8c50e75e3771e6b66
--- /dev/null
+++ b/lib/service/geospatial/google_maps.ex
@@ -0,0 +1,126 @@
+defmodule Mobilizon.Service.Geospatial.GoogleMaps do
+  @moduledoc """
+  Google Maps [Geocoding service](https://developers.google.com/maps/documentation/geocoding/intro)
+
+  Note: Endpoint is hardcoded to Google Maps API
+  """
+  alias Mobilizon.Service.Geospatial.Provider
+  alias Mobilizon.Addresses.Address
+  require Logger
+
+  @behaviour Provider
+
+  @api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
+
+  @components [
+    "street_number",
+    "route",
+    "locality",
+    "administrative_area_level_1",
+    "country",
+    "postal_code"
+  ]
+
+  @api_key_missing_message "API Key required to use Google Maps"
+
+  @impl Provider
+  @doc """
+  Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
+  """
+  @spec geocode(String.t(), keyword()) :: list(Address.t())
+  def geocode(lon, lat, options \\ []) do
+    url = build_url(:geocode, %{lon: lon, lat: lat}, options)
+
+    Logger.debug("Asking Google Maps for reverse geocode with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
+      Enum.map(results, &process_data/1)
+    else
+      {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
+        raise ArgumentError, message: to_string(error_message)
+    end
+  end
+
+  @impl Provider
+  @doc """
+  Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
+  """
+  @spec search(String.t(), keyword()) :: list(Address.t())
+  def search(q, options \\ []) do
+    url = build_url(:search, %{q: q}, options)
+
+    Logger.debug("Asking Google Maps for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
+      Enum.map(results, fn entry -> process_data(entry) end)
+    else
+      {:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
+        raise ArgumentError, message: to_string(error_message)
+    end
+  end
+
+  @spec build_url(atom(), map(), list()) :: String.t()
+  defp build_url(method, args, options) do
+    limit = Keyword.get(options, :limit, 10)
+    lang = Keyword.get(options, :lang, "en")
+    api_key = Keyword.get(options, :api_key, @api_key)
+    if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
+
+    url =
+      "https://maps.googleapis.com/maps/api/geocode/json?limit=#{limit}&key=#{api_key}&language=#{
+        lang
+      }"
+
+    case method do
+      :search ->
+        url <> "&address=#{URI.encode(args.q)}"
+
+      :geocode ->
+        url <> "&latlng=#{args.lat},#{args.lon}"
+    end
+  end
+
+  defp process_data(%{
+         "formatted_address" => description,
+         "geometry" => %{"location" => %{"lat" => lat, "lng" => lon}},
+         "address_components" => components
+       }) do
+    components =
+      @components
+      |> Enum.reduce(%{}, fn component, acc ->
+        Map.put(acc, component, extract_component(components, component))
+      end)
+
+    %Address{
+      addressCountry: Map.get(components, "country"),
+      addressLocality: Map.get(components, "locality"),
+      addressRegion: Map.get(components, "administrative_area_level_1"),
+      description: description,
+      floor: nil,
+      geom: [lon, lat] |> Provider.coordinates(),
+      postalCode: Map.get(components, "postal_code"),
+      streetAddress: street_address(components)
+    }
+  end
+
+  defp extract_component(components, key) do
+    case components
+         |> Enum.filter(fn component -> key in component["types"] end)
+         |> Enum.map(& &1["long_name"]) do
+      [] -> nil
+      component -> hd(component)
+    end
+  end
+
+  defp street_address(body) do
+    if Map.has_key?(body, "street_number") && !is_nil(Map.get(body, "street_number")) do
+      Map.get(body, "street_number") <> " " <> Map.get(body, "route")
+    else
+      Map.get(body, "route")
+    end
+  end
+end
diff --git a/lib/service/geospatial/map_quest.ex b/lib/service/geospatial/map_quest.ex
new file mode 100644
index 0000000000000000000000000000000000000000..ab9dc6f18af503231f78a95dce58967e74a73d4b
--- /dev/null
+++ b/lib/service/geospatial/map_quest.ex
@@ -0,0 +1,116 @@
+defmodule Mobilizon.Service.Geospatial.MapQuest do
+  @moduledoc """
+  [MapQuest](https://developer.mapquest.com/documentation) backend.
+
+  ## Options
+  In addition to the [the shared options](Mobilizon.Service.Geospatial.Provider.html#module-shared-options),
+  MapQuest methods support the following options:
+  * `:open_data` Whether to use [Open Data or Licenced Data](https://developer.mapquest.com/documentation/open/).
+    Defaults to `true`
+  """
+  alias Mobilizon.Service.Geospatial.Provider
+  alias Mobilizon.Addresses.Address
+  require Logger
+
+  @behaviour Provider
+
+  @api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
+
+  @api_key_missing_message "API Key required to use MapQuest"
+
+  @impl Provider
+  @doc """
+  MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
+  """
+  @spec geocode(String.t(), keyword()) :: list(Address.t())
+  def geocode(lon, lat, options \\ []) do
+    api_key = Keyword.get(options, :api_key, @api_key)
+    limit = Keyword.get(options, :limit, 10)
+    open_data = Keyword.get(options, :open_data, true)
+
+    prefix = if open_data, do: "open", else: "www"
+
+    if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(
+             "https://#{prefix}.mapquestapi.com/geocoding/v1/reverse?key=#{api_key}&location=#{
+               lat
+             },#{lon}&maxResults=#{limit}"
+           ),
+         {:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
+      results |> Enum.map(&processData/1)
+    else
+      {:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
+        raise(ArgumentError, message: err)
+    end
+  end
+
+  @impl Provider
+  @doc """
+  MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
+  """
+  @spec search(String.t(), keyword()) :: list(Address.t())
+  def search(q, options \\ []) do
+    limit = Keyword.get(options, :limit, 10)
+    api_key = Keyword.get(options, :api_key, @api_key)
+
+    open_data = Keyword.get(options, :open_data, true)
+
+    prefix = if open_data, do: "open", else: "www"
+
+    if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
+
+    url =
+      "https://#{prefix}.mapquestapi.com/geocoding/v1/address?key=#{api_key}&location=#{
+        URI.encode(q)
+      }&maxResults=#{limit}"
+
+    Logger.debug("Asking MapQuest for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
+      results |> Enum.map(&processData/1)
+    else
+      {:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
+        raise(ArgumentError, message: err)
+    end
+  end
+
+  defp processData(
+         %{
+           "locations" => addresses,
+           "providedLocation" => %{"latLng" => %{"lat" => lat, "lng" => lng}}
+         } = _body
+       ) do
+    case addresses do
+      [] -> nil
+      addresses -> addresses |> hd |> produceAddress(lat, lng)
+    end
+  end
+
+  defp processData(%{"locations" => addresses}) do
+    case addresses do
+      [] -> nil
+      addresses -> addresses |> hd |> produceAddress()
+    end
+  end
+
+  defp produceAddress(%{"latLng" => %{"lat" => lat, "lng" => lng}} = address) do
+    produceAddress(address, lat, lng)
+  end
+
+  defp produceAddress(address, lat, lng) do
+    %Address{
+      addressCountry: Map.get(address, "adminArea1"),
+      addressLocality: Map.get(address, "adminArea5"),
+      addressRegion: Map.get(address, "adminArea3"),
+      description: Map.get(address, "street"),
+      floor: Map.get(address, "floor"),
+      geom: [lng, lat] |> Provider.coordinates(),
+      postalCode: Map.get(address, "postalCode"),
+      streetAddress: Map.get(address, "street")
+    }
+  end
+end
diff --git a/lib/service/geospatial/nominatim.ex b/lib/service/geospatial/nominatim.ex
new file mode 100644
index 0000000000000000000000000000000000000000..fcbbab12cb07f6e063e35dfe8a3eeea6bcb98299
--- /dev/null
+++ b/lib/service/geospatial/nominatim.ex
@@ -0,0 +1,89 @@
+defmodule Mobilizon.Service.Geospatial.Nominatim do
+  @moduledoc """
+  [Nominatim](https://wiki.openstreetmap.org/wiki/Nominatim) backend.
+  """
+  alias Mobilizon.Service.Geospatial.Provider
+  alias Mobilizon.Addresses.Address
+  require Logger
+
+  @behaviour Provider
+
+  @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
+  @api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
+
+  @impl Provider
+  @doc """
+  Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
+  """
+  @spec geocode(String.t(), keyword()) :: list(Address.t())
+  def geocode(lon, lat, options \\ []) do
+    url = build_url(:geocode, %{lon: lon, lat: lat}, options)
+    Logger.debug("Asking Nominatim for geocode with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, body} <- Poison.decode(body) do
+      [process_data(body)]
+    end
+  end
+
+  @impl Provider
+  @doc """
+  Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
+  """
+  @spec search(String.t(), keyword()) :: list(Address.t())
+  def search(q, options \\ []) do
+    url = build_url(:search, %{q: q}, options)
+    Logger.debug("Asking Nominatim for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, body} <- Poison.decode(body) do
+      Enum.map(body, fn entry -> process_data(entry) end)
+    end
+  end
+
+  @spec build_url(atom(), map(), list()) :: String.t()
+  defp build_url(method, args, options) do
+    limit = Keyword.get(options, :limit, 10)
+    lang = Keyword.get(options, :lang, "en")
+    endpoint = Keyword.get(options, :endpoint, @endpoint)
+    api_key = Keyword.get(options, :api_key, @api_key)
+
+    url =
+      case method do
+        :search ->
+          "#{endpoint}/search?format=jsonv2&q=#{URI.encode(args.q)}&limit=#{limit}&accept-language=#{
+            lang
+          }&addressdetails=1"
+
+        :geocode ->
+          "#{endpoint}/reverse?format=jsonv2&lat=#{args.lat}&lon=#{args.lon}&addressdetails=1"
+      end
+
+    if is_nil(api_key), do: url, else: url <> "&key=#{api_key}"
+  end
+
+  @spec process_data(map()) :: Address.t()
+  defp process_data(%{"address" => address} = body) do
+    %Address{
+      addressCountry: Map.get(address, "country"),
+      addressLocality: Map.get(address, "city"),
+      addressRegion: Map.get(address, "state"),
+      description: Map.get(body, "display_name"),
+      floor: Map.get(address, "floor"),
+      geom: [Map.get(body, "lon"), Map.get(body, "lat")] |> Provider.coordinates(),
+      postalCode: Map.get(address, "postcode"),
+      streetAddress: street_address(address)
+    }
+  end
+
+  @spec street_address(map()) :: String.t()
+  defp street_address(body) do
+    if Map.has_key?(body, "house_number") do
+      Map.get(body, "house_number") <> " " <> Map.get(body, "road")
+    else
+      Map.get(body, "road")
+    end
+  end
+end
diff --git a/lib/service/geospatial/photon.ex b/lib/service/geospatial/photon.ex
new file mode 100644
index 0000000000000000000000000000000000000000..233694730a756c92aad3bf4419c25a16e3234661
--- /dev/null
+++ b/lib/service/geospatial/photon.ex
@@ -0,0 +1,87 @@
+defmodule Mobilizon.Service.Geospatial.Photon do
+  @moduledoc """
+  [Photon](https://photon.komoot.de) backend.
+  """
+  alias Mobilizon.Service.Geospatial.Provider
+  require Logger
+  alias Mobilizon.Addresses.Address
+
+  @behaviour Provider
+
+  @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
+
+  @impl Provider
+  @doc """
+  Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
+
+  Note: It seems results are quite wrong.
+  """
+  @spec geocode(number(), number(), keyword()) :: list(Address.t())
+  def geocode(lon, lat, options \\ []) do
+    url = build_url(:geocode, %{lon: lon, lat: lat}, options)
+    Logger.debug("Asking photon for reverse geocoding with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      processData(features)
+    end
+  end
+
+  @impl Provider
+  @doc """
+  Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
+  """
+  @spec search(String.t(), keyword()) :: list(Address.t())
+  def search(q, options \\ []) do
+    url = build_url(:search, %{q: q}, options)
+    Logger.debug("Asking photon for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url),
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      processData(features)
+    end
+  end
+
+  @spec build_url(atom(), map(), list()) :: String.t()
+  defp build_url(method, args, options) do
+    limit = Keyword.get(options, :limit, 10)
+    lang = Keyword.get(options, :lang, "en")
+    coords = Keyword.get(options, :coords, nil)
+    endpoint = Keyword.get(options, :endpoint, @endpoint)
+
+    case method do
+      :search ->
+        url = "#{endpoint}/api/?q=#{URI.encode(args.q)}&lang=#{lang}&limit=#{limit}"
+        if is_nil(coords), do: url, else: url <> "&lat=#{coords.lat}&lon=#{coords.lon}"
+
+      :geocode ->
+        "#{endpoint}/reverse?lon=#{args.lon}&lat=#{args.lat}&lang=#{lang}&limit=#{limit}"
+    end
+  end
+
+  defp processData(features) do
+    features
+    |> Enum.map(fn %{"geometry" => geometry, "properties" => properties} ->
+      %Address{
+        addressCountry: Map.get(properties, "country"),
+        addressLocality: Map.get(properties, "city"),
+        addressRegion: Map.get(properties, "state"),
+        description: Map.get(properties, "name") || streetAddress(properties),
+        floor: Map.get(properties, "floor"),
+        geom: Map.get(geometry, "coordinates") |> Provider.coordinates(),
+        postalCode: Map.get(properties, "postcode"),
+        streetAddress: properties |> streetAddress()
+      }
+    end)
+  end
+
+  defp streetAddress(properties) do
+    if Map.has_key?(properties, "housenumber") do
+      Map.get(properties, "housenumber") <> " " <> Map.get(properties, "street")
+    else
+      Map.get(properties, "street")
+    end
+  end
+end
diff --git a/lib/service/geospatial/provider.ex b/lib/service/geospatial/provider.ex
new file mode 100644
index 0000000000000000000000000000000000000000..e79ef2df84fc5dc5a155b26dd40b8e51441864e5
--- /dev/null
+++ b/lib/service/geospatial/provider.ex
@@ -0,0 +1,72 @@
+defmodule Mobilizon.Service.Geospatial.Provider do
+  @moduledoc """
+  Provider Behaviour for Geospatial stuff.
+
+  ## Supported backends
+
+  * `Mobilizon.Service.Geospatial.Nominatim` [🔗](https://wiki.openstreetmap.org/wiki/Nominatim)
+  * `Mobilizon.Service.Geospatial.Photon` [🔗](https://photon.komoot.de)
+  * `Mobilizon.Service.Geospatial.Addok` [🔗](https://github.com/addok/addok)
+  * `Mobilizon.Service.Geospatial.MapQuest` [🔗](https://developer.mapquest.com/documentation/open/)
+  * `Mobilizon.Service.Geospatial.GoogleMaps` [🔗](https://developers.google.com/maps/documentation/geocoding/intro)
+
+
+  ## Shared options
+
+  * `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"`
+  * `:lang` Lang in which to prefer results. Used as a request parameter or through an `Accept-Language` HTTP header.
+     Defaults to `"en"`.
+  * `:limit` Maximum limit for the number of results returned by the backend. Defaults to `10`
+  * `:api_key` Allows to override the API key (if the backend requires one) set inside the configuration.
+  * `:endpoint` Allows to override the endpoint set inside the configuration
+  """
+
+  alias Mobilizon.Addresses.Address
+
+  @doc """
+  Get an address from longitude and latitude coordinates.
+
+  ## Options
+
+  Most backends implement all of [the shared options](#module-shared-options).
+
+  ## Examples
+
+      iex> geocode(48.11, -1.77)
+      %Address{}
+  """
+  @callback geocode(longitude :: number(), latitude :: number(), options :: keyword()) ::
+              list(Address.t())
+
+  @doc """
+  Search for an address
+
+  ## Options
+
+  In addition to [the shared options](#module-shared-options), `c:search/2` also accepts the following options:
+
+  * `coords` Map of coordinates (ex: `%{lon: 48.11, lat: -1.77}`) allowing to give a geographic priority to the search.
+    Defaults to `nil`
+
+  ## Examples
+
+      iex> search("10 rue Jangot")
+      %Address{}
+  """
+  @callback search(address :: String.t(), options :: keyword()) :: list(Address.t())
+
+  @doc """
+  Returns a `Geo.Point` for given coordinates
+  """
+  @spec coordinates(list(number()), number()) :: Geo.Point.t()
+  def coordinates(coords, srid \\ 4326)
+
+  def coordinates([x, y], srid) when is_number(x) and is_number(y),
+    do: %Geo.Point{coordinates: {x, y}, srid: srid}
+
+  def coordinates([x, y], srid) when is_bitstring(x) and is_bitstring(y),
+    do: %Geo.Point{coordinates: {String.to_float(x), String.to_float(y)}, srid: srid}
+
+  @spec coordinates(any()) :: nil
+  def coordinates(_, _), do: nil
+end
diff --git a/mix.exs b/mix.exs
index f107b1dd709a0e35464c0c12a80ebd3088407e2d..596030d381d36228ceb5bfb67f26c577ac36acab 100644
--- a/mix.exs
+++ b/mix.exs
@@ -65,7 +65,6 @@ defmodule Mobilizon.Mixfile do
       {:geo, "~> 3.0"},
       {:geo_postgis, "~> 3.1"},
       {:timex, "~> 3.0"},
-      # Waiting for new release
       {:icalendar, "~> 0.7"},
       {:exgravatar, "~> 2.0.1"},
       {:httpoison, "~> 1.0"},
@@ -89,6 +88,7 @@ defmodule Mobilizon.Mixfile do
       {:atomex, "0.3.0"},
       {:cachex, "~> 3.1"},
       {:earmark, "~> 1.3.1"},
+      {:geohax, "~> 0.3.0"},
       # Dev and test dependencies
       {:phoenix_live_reload, "~> 1.2", only: :dev},
       {:ex_machina, "~> 2.2", only: [:dev, :test]},
diff --git a/mix.lock b/mix.lock
index 630d70370dc035c4d9d566b4dfc4911fba20692f..acd4947829deaf65052fc4d518a57f17c318d6f3 100644
--- a/mix.lock
+++ b/mix.lock
@@ -48,6 +48,7 @@
   "gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"},
   "geo": {:hex, :geo, "3.1.0", "727e005262430d037e870ff364e65d80ca5ca21d5ac8eddd57a1ada72c3f83b0", [:mix], [], "hexpm"},
   "geo_postgis": {:hex, :geo_postgis, "3.1.0", "d06c8fa5fd140a52a5c9dab4ad6623a696dd7d99dd791bb361d3f94942442ff9", [:mix], [{:geo, "~> 3.1", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
+  "geohax": {:hex, :geohax, "0.3.0", "c2e7d8cc6cdf4158120b50fcbe03a296da561d2089eb7ad68d84b6f5d3df5607", [:mix], [], "hexpm"},
   "geolix": {:hex, :geolix, "0.17.0", "8f3f4068be08599912de67ae24372a6c148794a0152f9f83ffd5a2ffcb21d29a", [:mix], [{:mmdb2_decoder, "~> 0.3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"},
   "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
   "guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
diff --git a/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json b/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json
new file mode 100644
index 0000000000000000000000000000000000000000..8216724dedc79b19e2eca99bfc5ccb7cc1bcc5bc
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/addok/geocode.json
@@ -0,0 +1,28 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://api-adresse.data.gouv.fr/reverse/?lon=4.842569&lat=45.751718"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"limit\": 1, \"features\": [{\"geometry\": {\"coordinates\": [4.842569, 45.751718], \"type\": \"Point\"}, \"properties\": {\"y\": 6518613.6, \"city\": \"Lyon\", \"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 1.0, \"distance\": 0, \"type\": \"housenumber\", \"street\": \"Rue Jangot\", \"name\": \"10 Rue Jangot\", \"x\": 843191.7, \"id\": \"69387_3650_f5ec2a\", \"housenumber\": \"10\", \"citycode\": \"69387\", \"context\": \"69, Rh\\u00f4ne, Auvergne-Rh\\u00f4ne-Alpes (Rh\\u00f4ne-Alpes)\", \"postcode\": \"69007\", \"importance\": 0.3164}, \"type\": \"Feature\"}], \"attribution\": \"BAN\", \"version\": \"draft\", \"type\": \"FeatureCollection\", \"licence\": \"ODbL 1.0\"}",
+      "headers": {
+        "Server": "nginx/1.13.4",
+        "Date": "Wed, 13 Mar 2019 17:22:17 GMT",
+        "Content-Type": "application/json; charset=utf-8",
+        "Content-Length": "598",
+        "Connection": "keep-alive",
+        "X-Cache-Status": "MISS",
+        "Access-Control-Allow-Origin": "*",
+        "Access-Control-Allow-Headers": "X-Requested-With"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/addok/search.json b/test/fixtures/vcr_cassettes/geospatial/addok/search.json
new file mode 100644
index 0000000000000000000000000000000000000000..bb395a96fe12773d6cb11ae4ff9bf422f07b9b53
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/addok/search.json
@@ -0,0 +1,29 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://api-adresse.data.gouv.fr/search/?q=10%20rue%20Jangot&limit=10"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"limit\": 10, \"features\": [{\"geometry\": {\"coordinates\": [4.842569, 45.751718], \"type\": \"Point\"}, \"properties\": {\"y\": 6518573.3, \"city\": \"Lyon\", \"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 0.8469454545454544, \"type\": \"housenumber\", \"street\": \"Rue Jangot\", \"name\": \"10 Rue Jangot\", \"x\": 843232.2, \"id\": \"ADRNIVX_0000000260022046\", \"housenumber\": \"10\", \"citycode\": \"69387\", \"context\": \"69, Rh\\u00f4ne, Auvergne-Rh\\u00f4ne-Alpes (Rh\\u00f4ne-Alpes)\", \"postcode\": \"69007\", \"importance\": 0.3164}, \"type\": \"Feature\"}, {\"geometry\": {\"coordinates\": [2.440118, 50.371066], \"type\": \"Point\"}, \"properties\": {\"y\": 7030518.3, \"city\": \"Bailleul-aux-Cornailles\", \"label\": \"Rue Jangon 62127 Bailleul-aux-Cornailles\", \"score\": 0.5039055944055943, \"name\": \"Rue Jangon\", \"x\": 660114.7, \"id\": \"62070_0100_9b8d3c\", \"type\": \"street\", \"citycode\": \"62070\", \"context\": \"62, Pas-de-Calais, Hauts-de-France (Nord-Pas-de-Calais)\", \"postcode\": \"62127\", \"importance\": 0.0045}, \"type\": \"Feature\"}], \"attribution\": \"BAN\", \"version\": \"draft\", \"type\": \"FeatureCollection\", \"licence\": \"ODbL 1.0\", \"query\": \"10 rue Jangot\"}",
+      "headers": {
+        "Server": "nginx/1.13.4",
+        "Date": "Wed, 13 Mar 2019 17:01:21 GMT",
+        "Content-Type": "application/json; charset=utf-8",
+        "Content-Length": "1087",
+        "Connection": "keep-alive",
+        "Vary": "Accept-Encoding",
+        "X-Cache-Status": "MISS",
+        "Access-Control-Allow-Origin": "*",
+        "Access-Control-Allow-Headers": "X-Requested-With"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json b/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json
new file mode 100644
index 0000000000000000000000000000000000000000..edfaacc022dd30fa5e89ee38feed914b4ec34a4f
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json
@@ -0,0 +1,33 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://maps.googleapis.com/maps/api/geocode/json?latlng=45.751718,4.842569&key=toto&language=en"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\n   \"plus_code\" : {\n      \"compound_code\" : \"QR2V+M2 Lyon, France\",\n      \"global_code\" : \"8FQ6QR2V+M2\"\n   },\n   \"results\" : [\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"10\",\n               \"short_name\" : \"10\",\n               \"types\" : [ \"street_number\" ]\n            },\n            {\n               \"long_name\" : \"Rue Jangot\",\n               \"short_name\" : \"Rue Jangot\",\n               \"types\" : [ \"route\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"location\" : {\n               \"lat\" : 45.751725,\n               \"lng\" : 4.8424967\n            },\n            \"location_type\" : \"ROOFTOP\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7530739802915,\n                  \"lng\" : 4.843845680291502\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7503760197085,\n                  \"lng\" : 4.841147719708498\n               }\n            }\n         },\n         \"place_id\" : \"ChIJrW0QikTq9EcRy4OdnHY6uS4\",\n         \"plus_code\" : {\n            \"compound_code\" : \"QR2R+MX Lyon, France\",\n            \"global_code\" : \"8FQ6QR2R+MX\"\n         },\n         \"types\" : [ \"establishment\", \"point_of_interest\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"10bis\",\n               \"short_name\" : \"10bis\",\n               \"types\" : [ \"street_number\" ]\n            },\n            {\n               \"long_name\" : \"Rue Jangot\",\n               \"short_name\" : \"Rue Jangot\",\n               \"types\" : [ \"route\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"10bis Rue Jangot, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"location\" : {\n               \"lat\" : 45.751725,\n               \"lng\" : 4.8424966\n            },\n            \"location_type\" : \"ROOFTOP\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7530739802915,\n                  \"lng\" : 4.843845580291503\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7503760197085,\n                  \"lng\" : 4.841147619708499\n               }\n            }\n         },\n         \"place_id\" : \"ChIJrW0QikTq9EcR96jk2OnO75w\",\n         \"plus_code\" : {\n            \"compound_code\" : \"QR2R+MX Lyon, France\",\n            \"global_code\" : \"8FQ6QR2R+MX\"\n         },\n         \"types\" : [ \"street_address\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"9\",\n               \"short_name\" : \"9\",\n               \"types\" : [ \"street_number\" ]\n            },\n            {\n               \"long_name\" : \"Rue Jangot\",\n               \"short_name\" : \"Rue Jangot\",\n               \"types\" : [ \"route\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"9 Rue Jangot, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"location\" : {\n               \"lat\" : 45.7518165,\n               \"lng\" : 4.8427168\n            },\n            \"location_type\" : \"RANGE_INTERPOLATED\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7531654802915,\n                  \"lng\" : 4.844065780291502\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7504675197085,\n                  \"lng\" : 4.841367819708497\n               }\n            }\n         },\n         \"place_id\" : \"EiA5IFJ1ZSBKYW5nb3QsIDY5MDA3IEx5b24sIEZyYW5jZSIaEhgKFAoSCR8N2ItE6vRHEW9tyPnhQsUIEAk\",\n         \"types\" : [ \"street_address\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"10\",\n               \"short_name\" : \"10\",\n               \"types\" : [ \"street_number\" ]\n            },\n            {\n               \"long_name\" : \"Rue Jangot\",\n               \"short_name\" : \"Rue Jangot\",\n               \"types\" : [ \"route\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7518154,\n                  \"lng\" : 4.8427199\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.75173520000001,\n                  \"lng\" : 4.8422361\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.7517676,\n               \"lng\" : 4.8424811\n            },\n            \"location_type\" : \"GEOMETRIC_CENTER\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7531242802915,\n                  \"lng\" : 4.843826980291502\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.75042631970851,\n                  \"lng\" : 4.841129019708498\n               }\n            }\n         },\n         \"place_id\" : \"ChIJadQ-ikTq9EcRo9mAXlItDZw\",\n         \"types\" : [ \"route\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"Saint Michel - Mairie\",\n               \"short_name\" : \"St Michel - Mairie\",\n               \"types\" : [ \"neighborhood\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"St Michel - Mairie, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.755675,\n                  \"lng\" : 4.846115999999999\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7458619,\n                  \"lng\" : 4.8380151\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.7481588,\n               \"lng\" : 4.841602099999999\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.755675,\n                  \"lng\" : 4.846115999999999\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7458619,\n                  \"lng\" : 4.8380151\n               }\n            }\n         },\n         \"place_id\" : \"ChIJ0zUnFUTq9EcRfrgAlAvv5ps\",\n         \"types\" : [ \"neighborhood\", \"political\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"7th arrondissement of Lyon\",\n               \"short_name\" : \"7th arrondissement of Lyon\",\n               \"types\" : [ \"political\", \"sublocality\", \"sublocality_level_1\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"7th arrondissement of Lyon, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.756743,\n                  \"lng\" : 4.8599059\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.707486,\n                  \"lng\" : 4.8187161\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.7304251,\n               \"lng\" : 4.8399378\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.756743,\n                  \"lng\" : 4.8599059\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.707486,\n                  \"lng\" : 4.8187161\n               }\n            }\n         },\n         \"place_id\" : \"ChIJ-_L7biTq9EcRIBnC5CqrCAU\",\n         \"types\" : [ \"political\", \"sublocality\", \"sublocality_level_1\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            }\n         ],\n         \"formatted_address\" : \"69007 Lyon, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7567599,\n                  \"lng\" : 4.8599819\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7073626,\n                  \"lng\" : 4.8187218\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.7304251,\n               \"lng\" : 4.8399378\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.7567599,\n                  \"lng\" : 4.8599819\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.7073626,\n                  \"lng\" : 4.8187218\n               }\n            }\n         },\n         \"place_id\" : \"ChIJ-_L7biTq9EcRcHzkQS6rCBw\",\n         \"types\" : [ \"postal_code\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            }\n         ],\n         \"formatted_address\" : \"Lyon, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.808425,\n                  \"lng\" : 4.898393\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.707486,\n                  \"lng\" : 4.7718489\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.764043,\n               \"lng\" : 4.835659\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.808425,\n                  \"lng\" : 4.898393\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.707486,\n                  \"lng\" : 4.7718489\n               }\n            }\n         },\n         \"place_id\" : \"ChIJl4foalHq9EcR8CG75CqrCAQ\",\n         \"types\" : [ \"locality\", \"political\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            }\n         ],\n         \"formatted_address\" : \"Rhône, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 46.30650199999999,\n                  \"lng\" : 5.1601089\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.45413,\n                  \"lng\" : 4.243647\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.7351456,\n               \"lng\" : 4.6108043\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 46.30650199999999,\n                  \"lng\" : 5.1601089\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.45413,\n                  \"lng\" : 4.243647\n               }\n            }\n         },\n         \"place_id\" : \"ChIJcf_obOGN9EcR0Cm55CqrCAM\",\n         \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            }\n         ],\n         \"formatted_address\" : \"Auvergne-Rhône-Alpes, France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 46.804293,\n                  \"lng\" : 7.1855661\n               },\n               \"southwest\" : {\n                  \"lat\" : 44.115493,\n                  \"lng\" : 2.062882\n               }\n            },\n            \"location\" : {\n               \"lat\" : 45.4471431,\n               \"lng\" : 4.385250699999999\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 46.804293,\n                  \"lng\" : 7.1855661\n               },\n               \"southwest\" : {\n                  \"lat\" : 44.115493,\n                  \"lng\" : 2.062882\n               }\n            }\n         },\n         \"place_id\" : \"ChIJ_fju9ukE9UcROuOAIn5wRjk\",\n         \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n      },\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            }\n         ],\n         \"formatted_address\" : \"France\",\n         \"geometry\" : {\n            \"bounds\" : {\n               \"northeast\" : {\n                  \"lat\" : 51.1241999,\n                  \"lng\" : 9.6624999\n               },\n               \"southwest\" : {\n                  \"lat\" : 41.31433,\n                  \"lng\" : -5.5591\n               }\n            },\n            \"location\" : {\n               \"lat\" : 46.227638,\n               \"lng\" : 2.213749\n            },\n            \"location_type\" : \"APPROXIMATE\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 51.1241999,\n                  \"lng\" : 9.6624999\n               },\n               \"southwest\" : {\n                  \"lat\" : 41.31433,\n                  \"lng\" : -5.5591\n               }\n            }\n         },\n         \"place_id\" : \"ChIJMVd4MymgVA0R99lHx5Y__Ws\",\n         \"types\" : [ \"country\", \"political\" ]\n      }\n   ],\n   \"status\" : \"OK\"\n}\n",
+      "headers": {
+        "Content-Type": "application/json; charset=UTF-8",
+        "Date": "Wed, 13 Mar 2019 17:53:53 GMT",
+        "Expires": "Thu, 14 Mar 2019 17:53:53 GMT",
+        "Cache-Control": "public, max-age=86400",
+        "Access-Control-Allow-Origin": "*",
+        "Server": "mafe",
+        "X-XSS-Protection": "1; mode=block",
+        "X-Frame-Options": "SAMEORIGIN",
+        "Server-Timing": "gfet4t7; dur=44",
+        "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,44,43,39\"",
+        "Accept-Ranges": "none",
+        "Vary": "Accept-Encoding",
+        "Transfer-Encoding": "chunked"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
diff --git a/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json b/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json
new file mode 100644
index 0000000000000000000000000000000000000000..9963a55cfede500c8cea12b83cb1ca488b50bc26
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/google_maps/search.json
@@ -0,0 +1,34 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://maps.googleapis.com/maps/api/geocode/json?address=10%20rue%20Jangot&limit=10&key=toto&language=en"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\n   \"results\" : [\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"10\",\n               \"short_name\" : \"10\",\n               \"types\" : [ \"street_number\" ]\n            },\n            {\n               \"long_name\" : \"Rue Jangot\",\n               \"short_name\" : \"Rue Jangot\",\n               \"types\" : [ \"route\" ]\n            },\n            {\n               \"long_name\" : \"Lyon\",\n               \"short_name\" : \"Lyon\",\n               \"types\" : [ \"locality\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Rhône\",\n               \"short_name\" : \"Rhône\",\n               \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"Auvergne-Rhône-Alpes\",\n               \"short_name\" : \"Auvergne-Rhône-Alpes\",\n               \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"France\",\n               \"short_name\" : \"FR\",\n               \"types\" : [ \"country\", \"political\" ]\n            },\n            {\n               \"long_name\" : \"69007\",\n               \"short_name\" : \"69007\",\n               \"types\" : [ \"postal_code\" ]\n            }\n         ],\n         \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n         \"geometry\" : {\n            \"location\" : {\n               \"lat\" : 45.75164940000001,\n               \"lng\" : 4.8424032\n            },\n            \"location_type\" : \"ROOFTOP\",\n            \"viewport\" : {\n               \"northeast\" : {\n                  \"lat\" : 45.75299838029151,\n                  \"lng\" : 4.843752180291502\n               },\n               \"southwest\" : {\n                  \"lat\" : 45.75030041970851,\n                  \"lng\" : 4.841054219708497\n               }\n            }\n         },\n         \"place_id\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n         \"plus_code\" : {\n            \"compound_code\" : \"QR2R+MX Lyon, France\",\n            \"global_code\" : \"8FQ6QR2R+MX\"\n         },\n         \"types\" : [ \"street_address\" ]\n      }\n   ],\n   \"status\" : \"OK\"\n}\n",
+      "headers": {
+        "Content-Type": "application/json; charset=UTF-8",
+        "Date": "Wed, 13 Mar 2019 17:50:19 GMT",
+        "Expires": "Thu, 14 Mar 2019 17:50:19 GMT",
+        "Access-Control-Allow-Origin": "*",
+        "Server": "mafe",
+        "X-XSS-Protection": "1; mode=block",
+        "X-Frame-Options": "SAMEORIGIN",
+        "Server-Timing": "gfet4t7; dur=52",
+        "Cache-Control": "public, max-age=86400",
+        "Age": "17",
+        "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,44,43,39\"",
+        "Accept-Ranges": "none",
+        "Vary": "Accept-Encoding",
+        "Transfer-Encoding": "chunked"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
diff --git a/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json b/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json
new file mode 100644
index 0000000000000000000000000000000000000000..a42c62f086075cd5d3b8ece6f35377f7cbea4149
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json
@@ -0,0 +1,36 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://open.mapquestapi.com/geocoding/v1/reverse?key=secret_key&location=45.751718,4.842569&maxResults=10"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":1,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"latLng\":{\"lat\":45.751718,\"lng\":4.842569}},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AAA\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=-570915433\"}]}]}",
+      "headers": {
+        "Access-Control-Allow-Methods": "OPTIONS,GET,POST",
+        "Access-Control-Allow-Origin": "*",
+        "Cache-Control": "no-cache, must-revalidate",
+        "Content-Type": "application/json;charset=UTF-8",
+        "Date": "Thu, 14 Mar 2019 09:27:01 GMT",
+        "Expires": "Mon, 20 Dec 1998 01:00:00 GMT",
+        "GeocodeTransactionCount": "0",
+        "Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT",
+        "Pragma": "no-cache",
+        "ReverseGeocodeTransactionCount": "1",
+        "Server": "Apache-Coyote/1.1",
+        "Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly",
+        "status": "success",
+        "transactionWeight": "1.0",
+        "Content-Length": "1063",
+        "Connection": "keep-alive"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
diff --git a/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json b/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json
new file mode 100644
index 0000000000000000000000000000000000000000..4b23ed257751c4105cc747cfd095872425302008
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/map_quest/search.json
@@ -0,0 +1,36 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":10,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"location\":\"10 rue Jangot\"},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"7e\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"Lyon\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AXX\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=1358091752\"}]}]}",
+      "headers": {
+        "Access-Control-Allow-Methods": "OPTIONS,GET,POST",
+        "Access-Control-Allow-Origin": "*",
+        "Cache-Control": "no-cache, must-revalidate",
+        "Content-Type": "application/json;charset=UTF-8",
+        "Date": "Thu, 14 Mar 2019 09:27:01 GMT",
+        "Expires": "Mon, 20 Dec 1998 01:00:00 GMT",
+        "GeocodeTransactionCount": "1",
+        "Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT",
+        "Pragma": "no-cache",
+        "ReverseGeocodeTransactionCount": "0",
+        "Server": "Apache-Coyote/1.1",
+        "Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly",
+        "status": "success",
+        "transactionWeight": "1.0",
+        "Content-Length": "1055",
+        "Connection": "keep-alive"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
new file mode 100644
index 0000000000000000000000000000000000000000..d8513a89d384254883ca4f55e13c79482a5ea175
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
@@ -0,0 +1,30 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=45.751718&lon=4.842569&addressdetails=1"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"place_id\":41453794,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":3078260611,\"lat\":\"45.7517141\",\"lon\":\"4.8425657\",\"place_rank\":30,\"category\":\"place\",\"type\":\"house\",\"importance\":\"0\",\"addresstype\":\"place\",\"name\":null,\"display_name\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France métropolitaine, 69007, France\",\"address\":{\"house_number\":\"10\",\"road\":\"Rue Jangot\",\"suburb\":\"La Guillotière\",\"city_district\":\"Lyon 7e Arrondissement\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state_district\":\"Circonscription départementale du Rhône\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"postcode\":\"69007\",\"country_code\":\"fr\"},\"boundingbox\":[\"45.7516141\",\"45.7518141\",\"4.8424657\",\"4.8426657\"]}",
+      "headers": {
+        "Date": "Thu, 14 Mar 2019 10:26:11 GMT",
+        "Server": "Apache/2.4.29 (Ubuntu)",
+        "Access-Control-Allow-Origin": "*",
+        "Access-Control-Allow-Methods": "OPTIONS,GET",
+        "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
+        "Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"",
+        "Upgrade": "h2",
+        "Connection": "Upgrade, close",
+        "Transfer-Encoding": "chunked",
+        "Content-Type": "application/json; charset=UTF-8"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
new file mode 100644
index 0000000000000000000000000000000000000000..c41b678cb4f8cbb97bb05d97d551c3060905b1ee
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
@@ -0,0 +1,30 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1"
+    },
+    "response": {
+      "binary": false,
+      "body": "[{\"place_id\":41453794,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":3078260611,\"boundingbox\":[\"45.7516641\",\"45.7517641\",\"4.8425157\",\"4.8426157\"],\"lat\":\"45.7517141\",\"lon\":\"4.8425657\",\"display_name\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"place_rank\":30,\"category\":\"place\",\"type\":\"house\",\"importance\":0.31100000000000005,\"address\":{\"house_number\":\"10\",\"road\":\"Rue Jangot\",\"suburb\":\"La Guillotière\",\"city_district\":\"Lyon 7e Arrondissement\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state_district\":\"Departemental constituency of Rhône\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"postcode\":\"69007\",\"country_code\":\"fr\"}}]",
+      "headers": {
+        "Date": "Thu, 14 Mar 2019 10:24:24 GMT",
+        "Server": "Apache/2.4.29 (Ubuntu)",
+        "Access-Control-Allow-Origin": "*",
+        "Access-Control-Allow-Methods": "OPTIONS,GET",
+        "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
+        "Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"",
+        "Upgrade": "h2",
+        "Connection": "Upgrade, close",
+        "Transfer-Encoding": "chunked",
+        "Content-Type": "application/json; charset=UTF-8"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json b/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json
new file mode 100644
index 0000000000000000000000000000000000000000..8ff49672e69bc17376f924eb576d611091f2c04b
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/photon/geocode.json
@@ -0,0 +1,26 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://photon.komoot.de/reverse?lon=4.842569&lat=45.751718"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8416864,45.7605435],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":4662865602,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"leisure\",\"city\":\"Lyon\",\"street\":\"Rue Pravaz\",\"osm_value\":\"fitness_centre\",\"postcode\":\"69003\",\"name\":\"L'appart Fitness\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}",
+      "headers": {
+        "Server": "nginx/1.9.3 (Ubuntu)",
+        "Date": "Thu, 14 Mar 2019 10:46:45 GMT",
+        "Content-Type": "application/json;charset=utf-8",
+        "Transfer-Encoding": "chunked",
+        "Connection": "keep-alive",
+        "Access-Control-Allow-Origin": "*"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/vcr_cassettes/geospatial/photon/search.json b/test/fixtures/vcr_cassettes/geospatial/photon/search.json
new file mode 100644
index 0000000000000000000000000000000000000000..45a644e63ff3111804aee92de5ea58561eadbad6
--- /dev/null
+++ b/test/fixtures/vcr_cassettes/geospatial/photon/search.json
@@ -0,0 +1,26 @@
+[
+  {
+    "request": {
+      "body": "",
+      "headers": [],
+      "method": "get",
+      "options": [],
+      "request_body": "",
+      "url": "https://photon.komoot.de/api/?q=10%20rue%20Jangot&lang=en&limit=10"
+    },
+    "response": {
+      "binary": false,
+      "body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8425657,45.7517141],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260611,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}},{\"geometry\":{\"coordinates\":[4.8424254,45.7517056],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260612,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10bis\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}",
+      "headers": {
+        "Server": "nginx/1.9.3 (Ubuntu)",
+        "Date": "Thu, 14 Mar 2019 10:46:43 GMT",
+        "Content-Type": "application/json;charset=utf-8",
+        "Transfer-Encoding": "chunked",
+        "Connection": "keep-alive",
+        "Access-Control-Allow-Origin": "*"
+      },
+      "status_code": 200,
+      "type": "ok"
+    }
+  }
+]
\ No newline at end of file
diff --git a/test/mobilizon/service/geospatial/addok_test.exs b/test/mobilizon/service/geospatial/addok_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..cbf9de19f64d5ca990f387300b3cc490bc9b5014
--- /dev/null
+++ b/test/mobilizon/service/geospatial/addok_test.exs
@@ -0,0 +1,59 @@
+defmodule Mobilizon.Service.Geospatial.AddokTest do
+  use Mobilizon.DataCase, async: false
+  alias Mobilizon.Service.Geospatial.Addok
+  alias Mobilizon.Addresses.Address
+
+  import Mock
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+
+  @endpoint Application.get_env(:mobilizon, Mobilizon.Service.Geospatial.Addok)
+            |> get_in([:endpoint])
+  @fake_endpoint "https://domain.tld"
+
+  describe "search address" do
+    test "produces a valid search address" do
+      with_mock HTTPoison, get: fn _url -> "{}" end do
+        Addok.search("10 Rue Jangot")
+        assert_called(HTTPoison.get("#{@endpoint}/search/?q=10%20Rue%20Jangot&limit=10"))
+      end
+    end
+
+    test "produces a valid search address with options" do
+      with_mock HTTPoison, get: fn _url -> "{}" end do
+        Addok.search("10 Rue Jangot",
+          endpoint: @fake_endpoint,
+          limit: 5,
+          coords: %{lat: 49, lon: 12}
+        )
+
+        assert_called(
+          HTTPoison.get("#{@fake_endpoint}/search/?q=10%20Rue%20Jangot&limit=5&lat=49&lon=12")
+        )
+      end
+    end
+
+    test "returns a valid address from search" do
+      use_cassette "geospatial/addok/search" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
+               } == Addok.search("10 rue Jangot") |> hd
+      end
+    end
+
+    test "returns a valid address from reverse geocode" do
+      use_cassette "geospatial/addok/geocode" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
+               } == Addok.geocode(4.842569, 45.751718) |> hd
+      end
+    end
+  end
+end
diff --git a/test/mobilizon/service/geospatial/geospatial_test.exs b/test/mobilizon/service/geospatial/geospatial_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..a43d02d7c870e9b6a33b51488f1e2e21cb393214
--- /dev/null
+++ b/test/mobilizon/service/geospatial/geospatial_test.exs
@@ -0,0 +1,8 @@
+defmodule Mobilizon.Service.GeospatialTest do
+  use Mobilizon.DataCase
+  alias Mobilizon.Service.Geospatial
+
+  describe "get service" do
+    assert Geospatial.service() === Elixir.Mobilizon.Service.Geospatial.Mock
+  end
+end
diff --git a/test/mobilizon/service/geospatial/google_maps_test.exs b/test/mobilizon/service/geospatial/google_maps_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..10cd84c76a4566aa577e24dfeda33b7e5f35e92c
--- /dev/null
+++ b/test/mobilizon/service/geospatial/google_maps_test.exs
@@ -0,0 +1,80 @@
+defmodule Mobilizon.Service.Geospatial.GoogleMapsTest do
+  use Mobilizon.DataCase, async: false
+  alias Mobilizon.Service.Geospatial.GoogleMaps
+  alias Mobilizon.Addresses.Address
+
+  import Mock
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+
+  describe "search address" do
+    test "without API Key triggers an error" do
+      assert_raise ArgumentError, "API Key required to use Google Maps", fn ->
+        GoogleMaps.search("10 Rue Jangot")
+      end
+    end
+
+    test "produces a valid search address with options" do
+      with_mock HTTPoison,
+        get: fn _url ->
+          {:ok,
+           %HTTPoison.Response{status_code: 200, body: "{\"status\": \"OK\", \"results\": []}"}}
+        end do
+        GoogleMaps.search("10 Rue Jangot",
+          limit: 5,
+          lang: "fr",
+          api_key: "toto"
+        )
+
+        assert_called(
+          HTTPoison.get(
+            "https://maps.googleapis.com/maps/api/geocode/json?limit=5&key=toto&language=fr&address=10%20Rue%20Jangot"
+          )
+        )
+      end
+    end
+
+    test "triggers an error with an invalid API Key" do
+      assert_raise ArgumentError, "The provided API key is invalid.", fn ->
+        GoogleMaps.search("10 rue Jangot", api_key: "secret_key")
+      end
+    end
+
+    test "returns a valid address from search" do
+      use_cassette "geospatial/google_maps/search" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot, 69007 Lyon, France",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "France",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.8424032, 45.75164940000001},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } == GoogleMaps.search("10 rue Jangot", api_key: "toto") |> hd
+      end
+    end
+
+    test "returns a valid address from reverse geocode" do
+      use_cassette "geospatial/google_maps/geocode" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot, 69007 Lyon, France",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "France",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.8424967, 45.751725},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } ==
+                 GoogleMaps.geocode(4.842569, 45.751718, api_key: "toto")
+                 |> hd
+      end
+    end
+  end
+end
diff --git a/test/mobilizon/service/geospatial/map_quest_test.exs b/test/mobilizon/service/geospatial/map_quest_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..6dc03684e3262de9ee7ac9aee3395ab660969e91
--- /dev/null
+++ b/test/mobilizon/service/geospatial/map_quest_test.exs
@@ -0,0 +1,85 @@
+defmodule Mobilizon.Service.Geospatial.MapQuestTest do
+  use Mobilizon.DataCase, async: false
+  alias Mobilizon.Service.Geospatial.MapQuest
+  alias Mobilizon.Addresses.Address
+
+  import Mock
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+
+  describe "search address" do
+    test "without API Key triggers an error" do
+      assert_raise ArgumentError, "API Key required to use MapQuest", fn ->
+        MapQuest.search("10 Rue Jangot")
+      end
+    end
+
+    test "produces a valid search address with options" do
+      with_mock HTTPoison,
+        get: fn _url ->
+          {:ok,
+           %HTTPoison.Response{
+             status_code: 200,
+             body: "{\"info\": {\"statuscode\": 0}, \"results\": []}"
+           }}
+        end do
+        MapQuest.search("10 Rue Jangot",
+          limit: 5,
+          lang: "fr",
+          api_key: "toto"
+        )
+
+        assert_called(
+          HTTPoison.get(
+            "https://open.mapquestapi.com/geocoding/v1/address?key=toto&location=10%20Rue%20Jangot&maxResults=5"
+          )
+        )
+      end
+    end
+
+    test "triggers an error with an invalid API Key" do
+      assert_raise ArgumentError, "The AppKey submitted with this request is invalid.", fn ->
+        MapQuest.search("10 rue Jangot", api_key: "secret_key")
+      end
+    end
+
+    test "returns a valid address from search" do
+      use_cassette "geospatial/map_quest/search" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "FR",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.842566, 45.751714},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } ==
+                 MapQuest.search("10 rue Jangot", api_key: "secret_key")
+                 |> hd
+      end
+    end
+
+    test "returns a valid address from reverse geocode" do
+      use_cassette "geospatial/map_quest/geocode" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "FR",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.842569, 45.751718},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } ==
+                 MapQuest.geocode(4.842569, 45.751718, api_key: "secret_key")
+                 |> hd
+      end
+    end
+  end
+end
diff --git a/test/mobilizon/service/geospatial/nominatim_test.exs b/test/mobilizon/service/geospatial/nominatim_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..29964ad565ff7857d89eb7d84e98d0dd4536f657
--- /dev/null
+++ b/test/mobilizon/service/geospatial/nominatim_test.exs
@@ -0,0 +1,68 @@
+defmodule Mobilizon.Service.Geospatial.NominatimTest do
+  use Mobilizon.DataCase, async: false
+  alias Mobilizon.Service.Geospatial.Nominatim
+  alias Mobilizon.Addresses.Address
+
+  import Mock
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+
+  describe "search address" do
+    test "produces a valid search address with options" do
+      with_mock HTTPoison,
+        get: fn _url ->
+          {:ok, %HTTPoison.Response{status_code: 200, body: "[]"}}
+        end do
+        Nominatim.search("10 Rue Jangot",
+          limit: 5,
+          lang: "fr"
+        )
+
+        assert_called(
+          HTTPoison.get(
+            "https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1"
+          )
+        )
+      end
+    end
+
+    test "returns a valid address from search" do
+      use_cassette "geospatial/nominatim/search" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description:
+                   "10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "France",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.8425657, 45.7517141},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } == Nominatim.search("10 rue Jangot") |> hd
+      end
+    end
+
+    test "returns a valid address from reverse geocode" do
+      use_cassette "geospatial/nominatim/geocode" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description:
+                   "10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France métropolitaine, 69007, France",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "France",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.8425657, 45.7517141},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } ==
+                 Nominatim.geocode(4.842569, 45.751718)
+                 |> hd
+      end
+    end
+  end
+end
diff --git a/test/mobilizon/service/geospatial/photon_test.exs b/test/mobilizon/service/geospatial/photon_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..51db05fd48ce399d1ca12a6412c7b21a548fccaa
--- /dev/null
+++ b/test/mobilizon/service/geospatial/photon_test.exs
@@ -0,0 +1,65 @@
+defmodule Mobilizon.Service.Geospatial.PhotonTest do
+  use Mobilizon.DataCase, async: false
+  alias Mobilizon.Service.Geospatial.Photon
+  alias Mobilizon.Addresses.Address
+
+  import Mock
+  use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
+
+  describe "search address" do
+    test "produces a valid search address with options" do
+      with_mock HTTPoison,
+        get: fn _url ->
+          {:ok, %HTTPoison.Response{status_code: 200, body: "{\"features\": []"}}
+        end do
+        Photon.search("10 Rue Jangot",
+          limit: 5,
+          lang: "fr"
+        )
+
+        assert_called(
+          HTTPoison.get("https://photon.komoot.de/api/?q=10%20Rue%20Jangot&lang=fr&limit=5")
+        )
+      end
+    end
+
+    test "returns a valid address from search" do
+      use_cassette "geospatial/photon/search" do
+        assert %Address{
+                 addressLocality: "Lyon",
+                 description: "10 Rue Jangot",
+                 addressRegion: "Auvergne-Rhône-Alpes",
+                 addressCountry: "France",
+                 postalCode: "69007",
+                 streetAddress: "10 Rue Jangot",
+                 geom: %Geo.Point{
+                   coordinates: {4.8425657, 45.7517141},
+                   properties: %{},
+                   srid: 4326
+                 }
+               } == Photon.search("10 rue Jangot") |> hd
+      end
+    end
+
+    # Photon returns something quite wrong, so no need to test this right now.
+    #    test "returns a valid address from reverse geocode" do
+    #      use_cassette "geospatial/photon/geocode" do
+    #        assert %Address{
+    #                 addressLocality: "Lyon",
+    #                 description: "",
+    #                 addressRegion: "Auvergne-Rhône-Alpes",
+    #                 addressCountry: "France",
+    #                 postalCode: "69007",
+    #                 streetAddress: "10 Rue Jangot",
+    #                 geom: %Geo.Point{
+    #                   coordinates: {4.8425657, 45.7517141},
+    #                   properties: %{},
+    #                   srid: 4326
+    #                 }
+    #               } ==
+    #                 Photon.geocode(4.8425657, 45.7517141)
+    #                 |> hd
+    #      end
+    #    end
+  end
+end
diff --git a/test/mobilizon_web/resolvers/address_resolver_test.exs b/test/mobilizon_web/resolvers/address_resolver_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..dfaf79cca6994b9dccd649c94f5253c357b95656
--- /dev/null
+++ b/test/mobilizon_web/resolvers/address_resolver_test.exs
@@ -0,0 +1,65 @@
+defmodule MobilizonWeb.Resolvers.AddressResolverTest do
+  use MobilizonWeb.ConnCase
+  alias MobilizonWeb.AbsintheHelpers
+  import Mobilizon.Factory
+
+  describe "Address Resolver" do
+    test "search/3 search for addresses", %{conn: conn} do
+      address = insert(:address, description: "10 rue Jangot, Lyon")
+
+      query = """
+        {
+          searchAddress(query: "10 Rue Jangot") {
+            description,
+            geom
+          }
+        }
+      """
+
+      res =
+        conn
+        |> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
+
+      json_response(res, 200)["data"]["searchAddress"]
+      |> Enum.each(fn addr -> assert Map.get(addr, "description") == address.description end)
+    end
+
+    test "geocode/3 reverse geocodes coordinates", %{conn: conn} do
+      address =
+        insert(:address,
+          description: "10 rue Jangot, Lyon"
+        )
+
+      query = """
+        {
+          reverseGeocode(longitude: -23.01, latitude: 30.01) {
+            description,
+            geom
+          }
+        }
+      """
+
+      res =
+        conn
+        |> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
+
+      assert json_response(res, 200)["data"]["reverseGeocode"] == []
+
+      query = """
+        {
+          reverseGeocode(longitude: 45.75, latitude: 4.85) {
+            description,
+            geom
+          }
+        }
+      """
+
+      res =
+        conn
+        |> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
+
+      assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
+               address.description
+    end
+  end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 3584b3d5b074f7f3dfd38a960473566974d2696b..ba31afd4b41dd8f92bed61ed7b23b71d491d405f 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -70,7 +70,7 @@ defmodule Mobilizon.Factory do
   def address_factory do
     %Mobilizon.Addresses.Address{
       description: sequence("MyAddress"),
-      geom: %Geo.Point{coordinates: {30, -90}, srid: 4326},
+      geom: %Geo.Point{coordinates: {45.75, 4.85}, srid: 4326},
       floor: "Myfloor",
       addressCountry: "My Country",
       addressLocality: "My Locality",
diff --git a/test/support/mocks/geospatial_mock.ex b/test/support/mocks/geospatial_mock.ex
new file mode 100644
index 0000000000000000000000000000000000000000..f5a3de8b4e08bfd23a8e9d7aa00925b5f8c9565d
--- /dev/null
+++ b/test/support/mocks/geospatial_mock.ex
@@ -0,0 +1,15 @@
+defmodule Mobilizon.Service.Geospatial.Mock do
+  @moduledoc """
+  Mock for Geospatial Provider implementations
+  """
+  alias Mobilizon.Service.Geospatial.Provider
+  alias Mobilizon.Addresses.Address
+
+  @behaviour Provider
+
+  @impl Provider
+  def geocode(_lon, _lat, _options \\ []), do: []
+
+  @impl Provider
+  def search(_q, _options \\ []), do: [%Address{description: "10 rue Jangot, Lyon"}]
+end