From 0e7cf89492785da927db0da336fec0c835ffb2bb Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Wed, 6 Nov 2019 12:51:17 +0100
Subject: [PATCH 1/2] Remove floor

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/graphql/address.ts                         |  1 -
 js/src/graphql/event.ts                           |  1 -
 js/src/types/address.model.ts                     |  2 --
 js/src/views/Event/Event.vue                      |  2 +-
 lib/mobilizon/addresses/address.ex                |  3 ---
 lib/mobilizon_web/schema/address.ex               |  2 --
 lib/service/geospatial/addok.ex                   |  1 -
 lib/service/geospatial/google_maps.ex             |  1 -
 lib/service/geospatial/map_quest.ex               |  1 -
 lib/service/geospatial/nominatim.ex               |  1 -
 lib/service/geospatial/photon.ex                  |  1 -
 ...20191106114524_remove_floor_from_addresses.exs | 15 +++++++++++++++
 schema.graphql                                    |  8 +-------
 test/mobilizon/addresses/addresses_test.exs       |  5 -----
 test/support/factory.ex                           |  1 -
 15 files changed, 17 insertions(+), 28 deletions(-)
 create mode 100644 priv/repo/migrations/20191106114524_remove_floor_from_addresses.exs

diff --git a/js/src/graphql/address.ts b/js/src/graphql/address.ts
index 3035d9ad8..b45790051 100644
--- a/js/src/graphql/address.ts
+++ b/js/src/graphql/address.ts
@@ -8,7 +8,6 @@ export const ADDRESS = gql`
             id,
             description,
             geom,
-            floor,
             street,
             locality,
             postalCode,
diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts
index d4620d5ed..a26250b19 100644
--- a/js/src/graphql/event.ts
+++ b/js/src/graphql/event.ts
@@ -18,7 +18,6 @@ const participantQuery = `
 
 const physicalAddressQuery = `
   description,
-  floor,
   street,
   locality,
   postalCode,
diff --git a/js/src/types/address.model.ts b/js/src/types/address.model.ts
index b26bbfd38..8db566691 100644
--- a/js/src/types/address.model.ts
+++ b/js/src/types/address.model.ts
@@ -1,7 +1,6 @@
 export interface IAddress {
   id?: number;
   description: string;
-  floor: string;
   street: string;
   locality: string;
   postalCode: string;
@@ -15,7 +14,6 @@ export interface IAddress {
 export class Address implements IAddress {
   country: string = '';
   description: string = '';
-  floor: string = '';
   locality: string = '';
   postalCode: string = '';
   region: string = '';
diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue
index f96290106..465b3d8ec 100644
--- a/js/src/views/Event/Event.vue
+++ b/js/src/views/Event/Event.vue
@@ -116,7 +116,7 @@ import {ParticipantRole} from "@/types/event.model";
                   <div class="address" v-if="event.physicalAddress">
                     <address>
                       <span class="addressDescription" :title="event.physicalAddress.description">{{ event.physicalAddress.description }}</span>
-                      <span>{{ event.physicalAddress.floor }} {{ event.physicalAddress.street }}</span>
+                      <span>{{ event.physicalAddress.street }}</span>
                       <span>{{ event.physicalAddress.postalCode }} {{ event.physicalAddress.locality }}</span>
                     </address>
                     <span class="map-show-button" @click="showMap = !showMap" v-if="event.physicalAddress && event.physicalAddress.geom">
diff --git a/lib/mobilizon/addresses/address.ex b/lib/mobilizon/addresses/address.ex
index c7c6453e1..0b2de0930 100644
--- a/lib/mobilizon/addresses/address.ex
+++ b/lib/mobilizon/addresses/address.ex
@@ -14,7 +14,6 @@ defmodule Mobilizon.Addresses.Address do
           locality: String.t(),
           region: String.t(),
           description: String.t(),
-          floor: String.t(),
           geom: Geo.PostGIS.Geometry.t(),
           postal_code: String.t(),
           street: String.t(),
@@ -26,7 +25,6 @@ defmodule Mobilizon.Addresses.Address do
   @required_attrs [:url]
   @optional_attrs [
     :description,
-    :floor,
     :geom,
     :country,
     :locality,
@@ -42,7 +40,6 @@ defmodule Mobilizon.Addresses.Address do
     field(:locality, :string)
     field(:region, :string)
     field(:description, :string)
-    field(:floor, :string)
     field(:geom, Geo.PostGIS.Geometry)
     field(:postal_code, :string)
     field(:street, :string)
diff --git a/lib/mobilizon_web/schema/address.ex b/lib/mobilizon_web/schema/address.ex
index 9a9006f83..da2dbc5ca 100644
--- a/lib/mobilizon_web/schema/address.ex
+++ b/lib/mobilizon_web/schema/address.ex
@@ -7,7 +7,6 @@ defmodule MobilizonWeb.Schema.AddressType do
 
   object :address do
     field(:geom, :point, description: "The geocoordinates for the point where this address is")
-    field(:floor, :string, description: "The floor this event is at")
     field(:street, :string, description: "The address's street name (with number)")
     field(:locality, :string, description: "The address's locality")
     field(:postal_code, :string)
@@ -32,7 +31,6 @@ defmodule MobilizonWeb.Schema.AddressType do
   input_object :address_input do
     # Either a full picture object
     field(:geom, :point, description: "The geocoordinates for the point where this address is")
-    field(:floor, :string, description: "The floor this event is at")
     field(:street, :string, description: "The address's street name (with number)")
     field(:locality, :string, description: "The address's locality")
     field(:postal_code, :string)
diff --git a/lib/service/geospatial/addok.ex b/lib/service/geospatial/addok.ex
index 8dd88d52b..737b233dc 100644
--- a/lib/service/geospatial/addok.ex
+++ b/lib/service/geospatial/addok.ex
@@ -74,7 +74,6 @@ defmodule Mobilizon.Service.Geospatial.Addok do
         locality: Map.get(properties, "city"),
         region: Map.get(properties, "state"),
         description: Map.get(properties, "name") || street_address(properties),
-        floor: Map.get(properties, "floor"),
         geom: geometry |> Map.get("coordinates") |> Provider.coordinates(),
         postal_code: Map.get(properties, "postcode"),
         street: properties |> street_address()
diff --git a/lib/service/geospatial/google_maps.ex b/lib/service/geospatial/google_maps.ex
index 72054c095..1b2960d52 100644
--- a/lib/service/geospatial/google_maps.ex
+++ b/lib/service/geospatial/google_maps.ex
@@ -127,7 +127,6 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
       locality: Map.get(components, "locality"),
       region: Map.get(components, "administrative_area_level_1"),
       description: description,
-      floor: nil,
       geom: [lon, lat] |> Provider.coordinates(),
       postal_code: Map.get(components, "postal_code"),
       street: street_address(components),
diff --git a/lib/service/geospatial/map_quest.ex b/lib/service/geospatial/map_quest.ex
index 084836a03..5f567224d 100644
--- a/lib/service/geospatial/map_quest.ex
+++ b/lib/service/geospatial/map_quest.ex
@@ -115,7 +115,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
       locality: Map.get(address, "adminArea5"),
       region: Map.get(address, "adminArea3"),
       description: Map.get(address, "street"),
-      floor: Map.get(address, "floor"),
       geom: [lng, lat] |> Provider.coordinates(),
       postal_code: Map.get(address, "postalCode"),
       street: Map.get(address, "street")
diff --git a/lib/service/geospatial/nominatim.ex b/lib/service/geospatial/nominatim.ex
index c00e0e936..3f4a6b9c9 100644
--- a/lib/service/geospatial/nominatim.ex
+++ b/lib/service/geospatial/nominatim.ex
@@ -78,7 +78,6 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
       locality: Map.get(address, "city"),
       region: Map.get(address, "state"),
       description: description(body),
-      floor: Map.get(address, "floor"),
       geom: [Map.get(body, "lon"), Map.get(body, "lat")] |> Provider.coordinates(),
       postal_code: Map.get(address, "postcode"),
       street: street_address(address),
diff --git a/lib/service/geospatial/photon.ex b/lib/service/geospatial/photon.ex
index df954305c..a42971c8d 100644
--- a/lib/service/geospatial/photon.ex
+++ b/lib/service/geospatial/photon.ex
@@ -76,7 +76,6 @@ defmodule Mobilizon.Service.Geospatial.Photon do
         locality: Map.get(properties, "city"),
         region: Map.get(properties, "state"),
         description: Map.get(properties, "name") || street_address(properties),
-        floor: Map.get(properties, "floor"),
         geom: geometry |> Map.get("coordinates") |> Provider.coordinates(),
         postal_code: Map.get(properties, "postcode"),
         street: properties |> street_address()
diff --git a/priv/repo/migrations/20191106114524_remove_floor_from_addresses.exs b/priv/repo/migrations/20191106114524_remove_floor_from_addresses.exs
new file mode 100644
index 000000000..69112fc18
--- /dev/null
+++ b/priv/repo/migrations/20191106114524_remove_floor_from_addresses.exs
@@ -0,0 +1,15 @@
+defmodule Mobilizon.Storage.Repo.Migrations.RemoveFloorFromAddresses do
+  use Ecto.Migration
+
+  def up do
+    alter table(:addresses) do
+      remove(:floor)
+    end
+  end
+
+  def down do
+    alter table(:addresses) do
+      add(:floor, :string)
+    end
+  end
+end
diff --git a/schema.graphql b/schema.graphql
index fae69d34e..ec149f147 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -1,5 +1,5 @@
 # source: http://localhost:4000/api
-# timestamp: Wed Oct 30 2019 17:12:28 GMT+0100 (Central European Standard Time)
+# timestamp: Wed Nov 06 2019 12:50:45 GMT+0100 (Central European Standard Time)
 
 schema {
   query: RootQueryType
@@ -119,9 +119,6 @@ type Address {
   country: String
   description: String
 
-  """The floor this event is at"""
-  floor: String
-
   """The geocoordinates for the point where this address is"""
   geom: Point
   id: ID
@@ -141,9 +138,6 @@ input AddressInput {
   country: String
   description: String
 
-  """The floor this event is at"""
-  floor: String
-
   """The geocoordinates for the point where this address is"""
   geom: Point
   id: ID
diff --git a/test/mobilizon/addresses/addresses_test.exs b/test/mobilizon/addresses/addresses_test.exs
index 8be79b2f1..e4e3e8b1e 100644
--- a/test/mobilizon/addresses/addresses_test.exs
+++ b/test/mobilizon/addresses/addresses_test.exs
@@ -12,7 +12,6 @@ defmodule Mobilizon.AddressesTest do
       locality: "some addressLocality",
       region: "some addressRegion",
       description: "some description",
-      floor: "some floor",
       postal_code: "some postalCode",
       street: "some streetAddress",
       geom: %Geo.Point{coordinates: {10, -10}, srid: 4326}
@@ -22,7 +21,6 @@ defmodule Mobilizon.AddressesTest do
       locality: "some updated addressLocality",
       region: "some updated addressRegion",
       description: "some updated description",
-      floor: "some updated floor",
       postal_code: "some updated postalCode",
       street: "some updated streetAddress",
       geom: %Geo.Point{coordinates: {20, -20}, srid: 4326}
@@ -32,7 +30,6 @@ defmodule Mobilizon.AddressesTest do
     #   addressLocality: nil,
     #   addressRegion: nil,
     #   description: nil,
-    #   floor: nil,
     #   postalCode: nil,
     #   streetAddress: nil,
     #   geom: nil
@@ -54,7 +51,6 @@ defmodule Mobilizon.AddressesTest do
       assert address.locality == "some addressLocality"
       assert address.region == "some addressRegion"
       assert address.description == "some description"
-      assert address.floor == "some floor"
       assert address.postal_code == "some postalCode"
       assert address.street == "some streetAddress"
     end
@@ -66,7 +62,6 @@ defmodule Mobilizon.AddressesTest do
       assert address.locality == "some updated addressLocality"
       assert address.region == "some updated addressRegion"
       assert address.description == "some updated description"
-      assert address.floor == "some updated floor"
       assert address.postal_code == "some updated postalCode"
       assert address.street == "some updated streetAddress"
     end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 336eccdf2..371c73e1c 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -83,7 +83,6 @@ defmodule Mobilizon.Factory do
       description: sequence("MyAddress"),
       geom: %Geo.Point{coordinates: {45.75, 4.85}, srid: 4326},
       url: "http://mobilizon.test/address/#{Ecto.UUID.generate()}",
-      floor: "Myfloor",
       country: "My Country",
       locality: "My Locality",
       region: "My Region",

From c599a47d5887ec953ee3defea669115332c69dc2 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Fri, 8 Nov 2019 19:37:14 +0100
Subject: [PATCH 2/2] Introduce Mimirsbrunn geocoder and improve addresses &
 maps

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 config/config.exs                             |   3 +
 config/dev.exs                                |   2 +-
 js/package.json                               |   2 +
 .../components/Event/AddressAutoComplete.vue  | 241 +++++++++++++-----
 js/src/components/Map.vue                     |  58 ++++-
 .../Map/Vue2LeafletLocateControl.vue          |  47 ++++
 js/src/graphql/address.ts                     |  38 ++-
 js/src/graphql/config.ts                      |   8 +-
 js/src/graphql/event.ts                       |   4 +-
 js/src/i18n/en_US.json                        |   7 +-
 js/src/i18n/fr_FR.json                        |  51 ++--
 js/src/types/address.model.ts                 |  87 ++++++-
 js/src/types/config.model.ts                  |   6 +
 js/src/types/event.model.ts                   |   4 +-
 js/src/utils/.editorconfig                    |  22 ++
 js/src/utils/poiIcons.ts                      |  61 +++++
 js/src/views/Event/Event.vue                  |  78 +++---
 js/src/vue-apollo.ts                          |   9 +-
 js/yarn.lock                                  |  14 +-
 lib/mobilizon/addresses/address.ex            |   5 +-
 lib/mobilizon/events/event.ex                 |  21 +-
 lib/mobilizon_web/resolvers/address.ex        |  44 ++--
 lib/mobilizon_web/resolvers/config.ex         |  23 +-
 lib/mobilizon_web/schema/address.ex           |   5 +
 lib/mobilizon_web/schema/config.ex            |   8 +
 lib/service/geospatial/google_maps.ex         |   8 +-
 lib/service/geospatial/mimirsbrunn.ex         | 146 +++++++++++
 lib/service/geospatial/nominatim.ex           |  80 +++---
 lib/service/geospatial/provider.ex            |   1 +
 .../20191106141051_add_type_to_addresses.exs  |   9 +
 schema.graphql                                |  14 +-
 .../geospatial/nominatim/geocode.json         |  10 +-
 .../geospatial/nominatim/search.json          |  10 +-
 .../service/geospatial/nominatim_test.exs     |  67 ++---
 .../resolvers/address_resolver_test.exs       |  10 +-
 test/support/mocks/geospatial_mock.ex         |   4 +-
 36 files changed, 940 insertions(+), 267 deletions(-)
 create mode 100644 js/src/components/Map/Vue2LeafletLocateControl.vue
 create mode 100644 js/src/utils/.editorconfig
 create mode 100644 js/src/utils/poiIcons.ts
 create mode 100644 lib/service/geospatial/mimirsbrunn.ex
 create mode 100644 priv/repo/migrations/20191106141051_add_type_to_addresses.exs

diff --git a/config/config.exs b/config/config.exs
index 243c7c0b4..8bf3b54e3 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -137,6 +137,9 @@ config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps,
 config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
   api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil
 
+config :mobilizon, Mobilizon.Service.Geospatial.Mimirsbrunn,
+  endpoint: System.get_env("GEOSPATIAL_MIMIRSBRUNN_ENDPOINT") || nil
+
 config :mobilizon, Oban,
   repo: Mobilizon.Storage.Repo,
   prune: {:maxlen, 10_000},
diff --git a/config/dev.exs b/config/dev.exs
index ce28a21d0..ac5f5a921 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -52,7 +52,7 @@ config :mobilizon, MobilizonWeb.Endpoint,
 # Do not include metadata nor timestamps in development logs
 config :logger, :console, format: "[$level] $message\n", level: :debug
 
-config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.GoogleMaps
+config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
 
 # Set a higher stacktrace during development. Avoid configuring such
 # in production as building large stacktraces may be expensive.
diff --git a/js/package.json b/js/package.json
index 7af8d1cf1..f5e1d6220 100644
--- a/js/package.json
+++ b/js/package.json
@@ -26,6 +26,7 @@
     "graphql-tag": "^2.10.1",
     "intersection-observer": "^0.7.0",
     "leaflet": "^1.4.0",
+    "leaflet.locatecontrol": "^0.68.0",
     "lodash": "^4.17.11",
     "ngeohash": "^0.6.3",
     "register-service-worker": "^1.6.2",
@@ -44,6 +45,7 @@
   "devDependencies": {
     "@types/chai": "^4.2.3",
     "@types/leaflet": "^1.5.2",
+    "@types/leaflet.locatecontrol": "^0.60.7",
     "@types/lodash": "^4.14.141",
     "@types/mocha": "^5.2.6",
     "@vue/cli-plugin-babel": "^4.0.3",
diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue
index c3b83fd3e..8b3e6f246 100644
--- a/js/src/components/Event/AddressAutoComplete.vue
+++ b/js/src/components/Event/AddressAutoComplete.vue
@@ -1,125 +1,242 @@
 <template>
     <div>
-        <b-field :label="$t('Find an address')">
+        <b-field expanded>
+            <template slot="label">
+                {{ $t('Find an address') }}
+                <b-button v-if="!gettingLocation" size="is-small" icon-right="map-marker" @click="locateMe" />
+                <span v-else>{{ $t('Getting location') }}</span>
+            </template>
             <b-autocomplete
                     :data="data"
                     v-model="queryText"
                     :placeholder="$t('e.g. 10 Rue Jangot')"
-                    field="description"
+                    field="fullName"
                     :loading="isFetching"
                     @typing="getAsyncData"
                     icon="map-marker"
-                    @select="option => selected = option">
+                    expanded
+                    @select="updateSelected">
 
                 <template slot-scope="{option}">
-                    <b>{{ option.description }}</b><br />
-                    <i v-if="option.url != null">Local</i>
-                    <p>
-                        <small>{{ option.street }},&#32; {{ option.postalCode }} {{ option.locality }}</small>
-                    </p>
+                    <b-icon :icon="option.poiInfos.poiIcon.icon" />
+                    <b>{{ option.poiInfos.name }}</b><br />
+                    <small>{{ option.poiInfos.alternativeName }}</small>
                 </template>
                 <template slot="empty">
-                    <span v-if="queryText.length < 5">{{ $t('Please type at least 5 characters') }}</span>
-                    <span v-else-if="isFetching">{{ $t('Searching…') }}</span>
+                    <span v-if="isFetching">{{ $t('Searching…') }}</span>
                     <div v-else class="is-enabled">
-                        <span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
-                        <p class="control" @click="addressModalActive = true">
-                            <button type="button" class="button is-primary">{{ $t('Add') }}</button>
-                        </p>
+                        <span>{{ $t('No results for "{queryText}". You can try another search term or drag and drop the marker on the map', { queryText }) }}</span>
+<!--                        <p class="control" @click="openNewAddressModal">-->
+<!--                            <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
+<!--                        </p>-->
                     </div>
                 </template>
             </b-autocomplete>
         </b-field>
-        <b-modal :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">
-            <div class="modal-card" style="width: auto">
-                <header class="modal-card-head">
-                    <p class="modal-card-title">{{ $t('Add an address') }}</p>
-                </header>
-                <section class="modal-card-body">
-                    <form>
-                        <b-field :label="$t('Name')">
-                            <b-input aria-required="true" required v-model="selected.description" />
-                        </b-field>
+        <div class="map" v-if="selected && selected.geom">
+            <map-leaflet
+                    :coords="selected.geom"
+                    :marker="{ text: [selected.poiInfos.name, selected.poiInfos.alternativeName], icon: selected.poiInfos.poiIcon.icon}"
+                    :updateDraggableMarkerCallback="reverseGeoCode"
+                    :options="{ zoom: mapDefaultZoom }"
+                    :readOnly="false"
+            />
+        </div>
+<!--        <b-modal v-if="selected" :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">-->
+<!--            <div class="modal-card" style="width: auto">-->
+<!--                <header class="modal-card-head">-->
+<!--                    <p class="modal-card-title">{{ $t('Add an address') }}</p>-->
+<!--                </header>-->
+<!--                <section class="modal-card-body">-->
+<!--                    <form>-->
+<!--                        <b-field :label="$t('Name')">-->
+<!--                            <b-input aria-required="true" required v-model="selected.description" />-->
+<!--                        </b-field>-->
 
-                        <b-field :label="$t('Street')">
-                            <b-input v-model="selected.street" />
-                        </b-field>
+<!--                        <b-field :label="$t('Street')">-->
+<!--                            <b-input v-model="selected.street" />-->
+<!--                        </b-field>-->
 
-                        <b-field :label="$t('Postal Code')">
-                            <b-input v-model="selected.postalCode" />
-                        </b-field>
+<!--                        <b-field grouped>-->
+<!--                            <b-field :label="$t('Postal Code')">-->
+<!--                                <b-input v-model="selected.postalCode" />-->
+<!--                            </b-field>-->
 
-                        <b-field :label="$t('Locality')">
-                            <b-input v-model="selected.locality" />
-                        </b-field>
+<!--                            <b-field :label="$t('Locality')">-->
+<!--                                <b-input v-model="selected.locality" />-->
+<!--                            </b-field>-->
+<!--                        </b-field>-->
 
-                        <b-field :label="$t('Region')">
-                            <b-input v-model="selected.region" />
-                        </b-field>
+<!--                        <b-field grouped>-->
+<!--                            <b-field :label="$t('Region')">-->
+<!--                                <b-input v-model="selected.region" />-->
+<!--                            </b-field>-->
 
-                        <b-field :label="$t('Country')">
-                            <b-input v-model="selected.country" />
-                        </b-field>
-                    </form>
-                </section>
-                <footer class="modal-card-foot">
-                    <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>
-                </footer>
-            </div>
-        </b-modal>
+<!--                            <b-field :label="$t('Country')">-->
+<!--                                <b-input v-model="selected.country" />-->
+<!--                            </b-field>-->
+<!--                        </b-field>-->
+<!--                    </form>-->
+<!--                </section>-->
+<!--                <footer class="modal-card-foot">-->
+<!--                    <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>-->
+<!--                </footer>-->
+<!--            </div>-->
+<!--        </b-modal>-->
     </div>
 </template>
 <script lang="ts">
 import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
 import { Address, IAddress } from '@/types/address.model';
-import { ADDRESS } from '@/graphql/address';
+import { ADDRESS, REVERSE_GEOCODE } from '@/graphql/address';
 import { Modal } from 'buefy/dist/components/dialog';
+import { LatLng } from 'leaflet';
+
 @Component({
   components: {
+    'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
     Modal,
   },
 })
 export default class AddressAutoComplete extends Vue {
 
-  @Prop({ required: false, default: () => [] }) initialData!: IAddress[];
-  @Prop({ required: false }) value!: IAddress;
+  @Prop({ required: true }) value!: IAddress;
 
-  data: IAddress[] = this.initialData;
-  selected: IAddress|null = new Address();
+  data: IAddress[] = [];
+  selected!: IAddress;
   isFetching: boolean = false;
-  queryText: string = this.value && this.value.description || '';
+  queryText: string = this.value && (new Address(this.value)).fullName || '';
   addressModalActive: boolean = false;
+  private gettingLocation: boolean = false;
+  private location!: Position;
+  private gettingLocationError: any;
+  private mapDefaultZoom: number = 15;
+
+  @Watch('value')
+  updateEditing() {
+    this.selected = this.value;
+    const address = new Address(this.selected);
+    this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
+  }
 
   async getAsyncData(query) {
-    if (query.length < 5) {
+    if (!query.length) {
+      this.data = [];
+      this.selected = new Address();
+      return;
+    }
+
+    if (query.length < 3) {
       this.data = [];
       return;
     }
     this.isFetching = true;
     const result = await this.$apollo.query({
       query: ADDRESS,
-      fetchPolicy: 'no-cache',
-      variables: { query },
+      fetchPolicy: 'network-only',
+      variables: {
+        query,
+        locale: this.$i18n.locale,
+      },
     });
 
-    this.data = result.data.searchAddress as IAddress[];
+    this.data = result.data.searchAddress.map(address => new Address(address));
     this.isFetching = false;
   }
 
-  // Watch deep because of subproperties
-  @Watch('selected', { deep: true })
-  updateSelected() {
+  updateSelected(option) {
+    if (option == null) return;
+    this.selected = option;
+    console.log('update selected', this.selected);
     this.$emit('input', this.selected);
   }
 
   resetPopup() {
     this.selected = new Address();
   }
+
+  openNewAddressModal() {
+    this.resetPopup();
+    this.addressModalActive = true;
+  }
+
+  async reverseGeoCode(e: LatLng, zoom: Number) {
+    // If the position has been updated through autocomplete selection, no need to geocode it !
+    if (this.checkCurrentPosition(e)) return;
+    const result = await this.$apollo.query({
+      query: REVERSE_GEOCODE,
+      variables: {
+        latitude: e.lat,
+        longitude: e.lng,
+        zoom,
+        locale: this.$i18n.locale,
+      },
+    });
+
+    this.data = result.data.reverseGeocode.map(address => new Address(address));
+    const defaultAddress = new Address(this.data[0]);
+    this.selected = defaultAddress;
+    this.$emit('input', this.selected);
+    this.queryText = `${defaultAddress.poiInfos.name} ${defaultAddress.poiInfos.alternativeName}`;
+  }
+
+  checkCurrentPosition(e: LatLng) {
+    if (!this.selected || !this.selected.geom) return false;
+    const lat = parseFloat(this.selected.geom.split(';')[1]);
+    const lon = parseFloat(this.selected.geom.split(';')[0]);
+
+    return e.lat === lat && e.lng === lon;
+  }
+
+  async locateMe(): Promise<void> {
+
+    this.gettingLocation = true;
+    try {
+      this.gettingLocation = false;
+      this.location = await this.getLocation();
+      this.mapDefaultZoom = 12;
+      this.reverseGeoCode(new LatLng(this.location.coords.latitude, this.location.coords.longitude), 12);
+    } catch (e) {
+      this.gettingLocation = false;
+      this.gettingLocationError = e.message;
+    }
+  }
+
+  async getLocation(): Promise<Position> {
+    return new Promise((resolve, reject) => {
+
+      if (!('geolocation' in navigator)) {
+        reject(new Error('Geolocation is not available.'));
+      }
+
+      navigator.geolocation.getCurrentPosition(pos => {
+        resolve(pos);
+      },                                       err => {
+        reject(err);
+      });
+
+    });
+  }
 }
 </script>
 <style lang="scss">
-    .autocomplete .dropdown-item.is-disabled .is-enabled {
-        opacity: 1 !important;
-        cursor: auto;
+    .autocomplete {
+        .dropdown-menu {
+            z-index: 2000;
+        }
+
+        .dropdown-item.is-disabled {
+            opacity: 1 !important;
+            cursor: auto;
+        }
+    }
+
+    .read-only {
+        cursor: pointer;
+    }
+
+    .map {
+        height: 400px;
+        width: 100%;
     }
 </style>
diff --git a/js/src/components/Map.vue b/js/src/components/Map.vue
index d1c11d57e..9721cc763 100644
--- a/js/src/components/Map.vue
+++ b/js/src/components/Map.vue
@@ -5,40 +5,54 @@
                 :style="`height: ${mergedOptions.height}; width: ${mergedOptions.width}`"
                 class="leaflet-map"
                 :center="[lat, lon]"
+                @click="clickMap"
+                @update:zoom="updateZoom"
         >
             <l-tile-layer
-                    url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
-                    attribution="© OpenStreetMap contributors"
+                    url="https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"
+                    :attribution="$t('© The OpenStreetMap Contributors')"
             >
 
             </l-tile-layer>
-            <l-marker :lat-lng="[lat, lon]" >
-                <l-popup v-if="popup">{{ popup }}</l-popup>
+            <v-locatecontrol :options="{icon: 'mdi mdi-map-marker'}"/>
+            <l-marker :lat-lng="[lat, lon]" @add="openPopup" @update:latLng="updateDraggableMarkerPosition" :draggable="!readOnly">
+                <l-popup v-if="popupMultiLine">
+                    <span v-for="line in popupMultiLine" :key="line">{{ line }}<br /></span>
+                </l-popup>
             </l-marker>
         </l-map>
     </div>
 </template>
 
 <script lang="ts">
-import { Icon }  from 'leaflet';
+import { Icon, LatLng, LeafletMouseEvent } from 'leaflet';
 import 'leaflet/dist/leaflet.css';
 import { Component, Prop, Vue } from 'vue-property-decorator';
-import { LMap, LTileLayer, LMarker, LPopup } from 'vue2-leaflet';
+import { LMap, LTileLayer, LMarker, LPopup, LIcon } from 'vue2-leaflet';
+import Vue2LeafletLocateControl from '@/components/Map/Vue2LeafletLocateControl.vue';
 
 @Component({
-  components: { LTileLayer, LMap, LMarker, LPopup },
+  components: { LTileLayer, LMap, LMarker, LPopup, LIcon, 'v-locatecontrol': Vue2LeafletLocateControl },
 })
 export default class Map extends Vue {
+  @Prop({ type: Boolean, required: false, default: true }) readOnly!: boolean;
   @Prop({ type: String, required: true }) coords!: string;
-  @Prop({ type: String, required: false }) popup!: string;
+  @Prop({ type: Object, required: false }) marker!: { text: String|String[], icon: String };
   @Prop({ type: Object, required: false }) options!: object;
+  @Prop({ type: Function, required: false, default: () => {} }) updateDraggableMarkerCallback!: Function;
 
-  defaultOptions: object = {
+  defaultOptions: {
+    zoom: Number;
+    height: String;
+    width: String;
+  } = {
     zoom: 15,
     height: '100%',
     width: '100%',
   };
 
+  zoom = this.defaultOptions.zoom;
+
   mounted() {
     // this part resolve an issue where the markers would not appear
     // @ts-ignore
@@ -51,12 +65,38 @@ export default class Map extends Vue {
     });
   }
 
+  openPopup(event) {
+    this.$nextTick(() => {
+      event.target.openPopup();
+    });
+  }
+
   get mergedOptions(): object {
     return { ...this.defaultOptions, ...this.options };
   }
 
   get lat() { return this.$props.coords.split(';')[1]; }
   get lon() { return this.$props.coords.split(';')[0]; }
+
+  get popupMultiLine() {
+    if (Array.isArray(this.marker.text)) {
+      return this.marker.text;
+    }
+    return [this.marker.text];
+  }
+
+  clickMap(event: LeafletMouseEvent) {
+    this.updateDraggableMarkerPosition(event.latlng);
+  }
+
+  updateDraggableMarkerPosition(e: LatLng) {
+    console.log('updateDraggableMarkerPosition', e);
+    this.updateDraggableMarkerCallback(e, this.zoom);
+  }
+
+  updateZoom(zoom: Number) {
+    this.zoom = zoom;
+  }
 }
 </script>
 <style lang="scss" scoped>
diff --git a/js/src/components/Map/Vue2LeafletLocateControl.vue b/js/src/components/Map/Vue2LeafletLocateControl.vue
new file mode 100644
index 000000000..44a7884ff
--- /dev/null
+++ b/js/src/components/Map/Vue2LeafletLocateControl.vue
@@ -0,0 +1,47 @@
+<template>
+    <div style="display: none;">
+        <slot v-if="ready"></slot>
+    </div>
+</template>
+
+<script lang="ts">
+/**
+ * Fork of https://github.com/domoritz/leaflet-locatecontrol to try to trigger location manually (not done ATM)
+ */
+
+import L, { DomEvent } from 'leaflet';
+import { findRealParent, propsBinder } from 'vue2-leaflet';
+import 'leaflet.locatecontrol';
+import { Component, Prop, Vue } from 'vue-property-decorator';
+
+@Component({
+  beforeDestroy() {
+        // @ts-ignore
+    this.parentContainer.removeLayer(this);
+  },
+})
+export default class Vue2LeafletLocateControl extends Vue {
+  @Prop({ type: Object, default: () => { return {}; } }) options;
+  @Prop({ type: Boolean, default: true }) visible = true;
+  ready: boolean = false;
+  mapObject!: any;
+  parentContainer: any;
+
+  mounted() {
+    this.mapObject = L.control.locate(this.options);
+    DomEvent.on(this.mapObject, this.$listeners as any);
+    propsBinder(this, this.mapObject, this.$props);
+    this.ready = true;
+    this.parentContainer = findRealParent(this.$parent);
+    this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
+  }
+
+  public locate() {
+    this.mapObject.start();
+  }
+}
+</script>
+
+<style>
+    @import "~leaflet.locatecontrol/dist/L.Control.Locate.css";
+</style>
diff --git a/js/src/graphql/address.ts b/js/src/graphql/address.ts
index b45790051..9f3857eb5 100644
--- a/js/src/graphql/address.ts
+++ b/js/src/graphql/address.ts
@@ -1,20 +1,34 @@
 import gql from 'graphql-tag';
 
+const $addressFragment = `
+id,
+description,
+geom,
+street,
+locality,
+postalCode,
+region,
+country,
+type,
+url,
+originId
+`;
+
 export const ADDRESS = gql`
-    query($query:String!) {
+    query($query:String!, $locale: String) {
         searchAddress(
-            query: $query
+            query: $query,
+            locale: $locale
         ) {
-            id,
-            description,
-            geom,
-            street,
-            locality,
-            postalCode,
-            region,
-            country,
-            url,
-            originId
+            ${$addressFragment}
+        }
+    }
+`;
+
+export const REVERSE_GEOCODE = gql`
+    query($latitude: Float!, $longitude: Float!, $zoom: Int, $locale: String) {
+        reverseGeocode(latitude: $latitude, longitude: $longitude, zoom: $zoom, locale: $locale) {
+            ${$addressFragment}
         }
     }
 `;
diff --git a/js/src/graphql/config.ts b/js/src/graphql/config.ts
index 3258080b1..ff1f3f227 100644
--- a/js/src/graphql/config.ts
+++ b/js/src/graphql/config.ts
@@ -5,7 +5,13 @@ query {
   config {
     name,
     description,
-    registrationsOpen
+    registrationsOpen,
+    countryCode,
+    location {
+      latitude,
+      longitude,
+      accuracyRadius
+    }
   }
 }
 `;
diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts
index a26250b19..b970e7300 100644
--- a/js/src/graphql/event.ts
+++ b/js/src/graphql/event.ts
@@ -24,7 +24,9 @@ const physicalAddressQuery = `
   region,
   country,
   geom,
-  id
+  type,
+  id,
+  originId
 `;
 
 const tagsQuery = `
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index fd0f92998..8955f1ede 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -110,6 +110,7 @@
 	"From the {startDate} to the {endDate}": "From the {startDate} to the {endDate}",
 	"Gather ⋅ Organize ⋅ Mobilize": "Gather ⋅ Organize ⋅ Mobilize",
 	"General information": "General information",
+	"Getting location": "Getting location",
 	"Going as {name}": "Going as {name}",
 	"Group List": "Group List",
 	"Group full name": "Group full name",
@@ -160,7 +161,7 @@
 	"No events found": "No events found",
 	"No group found": "No group found",
 	"No groups found": "No groups found",
-	"No results for \"{queryText}\"": "No results for \"{queryText}\"",
+	"No results for \"{queryText}\". You can try another search term or drag and drop the marker on the map": "No results for \"{queryText}\". You can try another search term or drag and drop the marker on the map",
 	"No user account with this email was found. Maybe you made a typo?": "No user account with this email was found. Maybe you made a typo?",
 	"Number of places": "Number of places",
 	"OK": "OK",
@@ -195,7 +196,6 @@
 	"Please make sure the address is correct and that the page hasn't been moved.": "Please make sure the address is correct and that the page hasn't been moved.",
 	"Please read the full rules": "Please read the full rules",
 	"Please refresh the page and retry.": "Please refresh the page and retry.",
-	"Please type at least 5 characters": "Please type at least 5 characters",
 	"Postal Code": "Postal Code",
 	"Private event": "Private event",
 	"Private feeds": "Private feeds",
@@ -327,5 +327,6 @@
 	"{count} participants": "No participants yet | One participant | {count} participants",
 	"{count} requests waiting": "{count} requests waiting",
 	"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
-	"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
+	"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks",
+	"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors"
 }
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index 46075f759..1cd12072e 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -3,14 +3,14 @@
     "A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
     "A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
     "Abandon edition": "Abandonner l'édition",
-    "About": "À propos",
     "About Mobilizon": "À propos de Mobilizon",
     "About this event": "À propos de cet événement",
     "About this instance": "À propos de cette instance",
-    "Add": "Ajouter",
+    "About": "À propos",
     "Add an address": "Ajouter une adresse",
     "Add some tags": "Ajouter des tags",
     "Add to my calendar": "Ajouter à mon agenda",
+    "Add": "Ajouter",
     "Additional comments": "Commentaires additionnels",
     "Administration": "Administration",
     "All data will be deleted every 48 hours, so please don't use this for anything real.": "Toutes les données seront effacées toutes les 48 heures, donc n'utilisez pas ce site à des fins autres que de démonstration.",
@@ -25,28 +25,27 @@
     "Avatar": "Avatar",
     "Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
     "By {name}": "Par {name}",
-    "Cancel": "Annuler",
     "Cancel creation": "Annuler la création",
     "Cancel edition": "Annuler l'édition",
     "Cancel my participation request…": "Annuler ma demande de participation…",
     "Cancel my participation…": "Annuler ma participation…",
-    "Cancelled: Won't happen": "Annulé : N'aura pas lieu",
+    "Cancel": "Annuler",
+    "Cancelled: Won't happen": "Annulé : N'aura pas lieu",
     "Category": "Catégorie",
-    "Change": "Modifier",
     "Change my identity…": "Changer mon identité…",
     "Change my password": "Modifier mon mot de passe",
     "Change password": "Modifier mot de passe",
+    "Change": "Modifier",
     "Clear": "Effacer",
     "Click to select": "Cliquez pour sélectionner",
     "Click to upload": "Cliquez pour uploader",
     "Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)",
-    "Comments": "Commentaires",
     "Comments on the event page": "Commentaires sur la page de l'événement",
+    "Comments": "Commentaires",
     "Confirm my particpation": "Confirmer ma participation",
     "Confirmed: Will happen": "Confirmé : aura lieu",
     "Continue editing": "Continuer l'édition",
     "Country": "Pays",
-    "Create": "Créer",
     "Create a new event": "Créer un nouvel événement",
     "Create a new group": "Créer un nouveau groupe",
     "Create a new identity": "Créer une nouvelle identité",
@@ -57,16 +56,17 @@
     "Create my profile": "Créer mon profil",
     "Create token": "Créer un jeton",
     "Create, edit or delete events": "Créer, modifier ou supprimer des événements",
+    "Create": "Créer",
     "Creator": "Créateur",
     "Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet événement.",
     "Date and time settings": "Paramètres de date et d'heure",
     "Date parameters": "Paramètres de date",
-    "Delete": "Supprimer",
     "Delete event": "Supprimer un événement",
     "Delete this identity": "Supprimer cette identité",
     "Delete your identity": "Supprimer votre identité",
     "Delete {eventTitle}": "Supprimer {eventTitle}",
     "Delete {preferredUsername}": "Supprimer {preferredUsername}",
+    "Delete": "Supprimer",
     "Description": "Description",
     "Didn't receive the instructions ?": "Vous n'avez pas reçu les instructions ?",
     "Display name": "Nom affiché",
@@ -84,7 +84,6 @@
     "Error while communicating with the server.": "Erreur de communication avec le serveur.",
     "Error while saving report.": "Erreur lors de l'enregistrement du signalement.",
     "Error while validating account": "Erreur lors de la validation du compte",
-    "Event": "Événement",
     "Event already passed": "Événement déjà passé",
     "Event cancelled": "Événement annulé",
     "Event creation": "Création d'événement",
@@ -95,6 +94,7 @@
     "Event to be confirmed": "Événement à confirmer",
     "Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
     "Event {eventTitle} reported": "Événement {eventTitle} signalé",
+    "Event": "Événement",
     "Events": "Événements",
     "Exclude": "Exclure",
     "Explore": "Explorer",
@@ -102,14 +102,15 @@
     "Features": "Fonctionnalités",
     "Find an address": "Trouver une adresse",
     "Find an instance": "Trouver une instance",
-    "For instance: London, Taekwondo, Architecture…": "Par exemple : Lyon, Taekwondo, Architecture…",
+    "For instance: London, Taekwondo, Architecture…": "Par exemple : Lyon, Taekwondo, Architecture…",
     "Forgot your password ?": "Mot de passe oublié ?",
     "From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants’ platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "De l’anniversaire entre ami·e·s à une marche pour le climat, aujourd’hui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment s’organiser, comment cliquer sur « je participe » sans <b>livrer des données intimes</b> à Facebook ou<b> s’enfermer</b> dans MeetUp ?",
-    "From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
     "From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
+    "From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
     "From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
     "Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser",
     "General information": "Informations générales",
+    "Getting location": "Récupération de la position",
     "Going as {name}": "En tant que {name}",
     "Group List": "Liste de groupes",
     "Group full name": "Nom complet du groupe",
@@ -131,8 +132,8 @@
     "Join {instance}, a Mobilizon instance": "Rejoignez {instance}, une instance Mobilizon",
     "Last published event": "Dernier événement publié",
     "Last week": "La semaine dernière",
-    "Learn more": "En apprendre plus",
     "Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon",
+    "Learn more": "En apprendre plus",
     "Leave event": "Annuler ma participation à l'événement",
     "Leaving event \"{title}\"": "Annuler ma participation à l'événement",
     "Let's create a new common": "Créons un nouveau Common",
@@ -142,8 +143,8 @@
     "Locality": "Commune",
     "Log in": "Se connecter",
     "Log out": "Se déconnecter",
-    "Login": "Se connecter",
     "Login on Mobilizon!": "Se connecter sur Mobilizon !",
+    "Login": "Se connecter",
     "Manage participations": "Gérer les participations",
     "Members": "Membres",
     "Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.": "Mobilizon est un logiciel libre qui permettra à des communautés de <b>créer leurs propres espaces</b> de publication d’événements, afin de mieux s’émanciper des géants du web.",
@@ -161,19 +162,20 @@
     "No group found": "Aucun groupe trouvé",
     "No groups found": "Aucun groupe trouvé",
     "No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
+    "No results for \"{queryText}\". You can try another search term or drag and drop the marker on the map": "Pas de résultats pour « {queryText} ». Vous pouvez essayer avec d'autres termes de recherche ou bien glisser et déposer le marqueur sur la carte",
     "No user account with this email was found. Maybe you made a typo?": "Aucun compte utilisateur trouvé pour cet email. Peut-être avez-vous fait une faute de frappe ?",
     "Number of places": "Nombre de places",
     "OK": "OK",
     "Old password": "Ancien mot de passe",
-    "On {date}": "Le {date}",
     "On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}",
     "On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
     "On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
+    "On {date}": "Le {date}",
     "One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
     "Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
     "Opened reports": "Signalements ouverts",
-    "Organized": "Organisés",
     "Organized by {name}": "Organisé par {name}",
+    "Organized": "Organisés",
     "Organizer": "Organisateur",
     "Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
     "Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
@@ -184,10 +186,10 @@
     "Participate": "Participer",
     "Participation approval": "Validation des participations",
     "Participation requested!": "Participation demandée !",
-    "Password": "Mot de passe",
     "Password (confirmation)": "Mot de passe (confirmation)",
     "Password change": "Changement de mot de passe",
     "Password reset": "Réinitialisation du mot de passe",
+    "Password": "Mot de passe",
     "Past events": "Événements passés",
     "Pick an identity": "Choisissez une identité",
     "Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.",
@@ -209,23 +211,23 @@
     "RSS/Atom Feed": "Flux RSS/Atom",
     "Read Framasoft’s statement of intent on the Framablog": "Lire la note d’intention de Framasoft sur le Framablog",
     "Region": "Région",
-    "Register": "S'inscrire",
     "Register an account on Mobilizon!": "S'inscrire sur Mobilizon !",
     "Register for an event by choosing one of your identities": "S'inscrire à un événement en choisissant une de vos identités",
+    "Register": "S'inscrire",
     "Registration is currently closed.": "Les inscriptions sont actuellement fermées.",
     "Reject": "Rejetter",
-    "Rejected": "Rejetés",
     "Rejected participations": "Participations rejetées",
-    "Report": "Signaler",
+    "Rejected": "Rejetés",
     "Report this event": "Signaler cet événement",
+    "Report": "Signaler",
     "Requests": "Requêtes",
     "Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
     "Reset my password": "Réinitialiser mon mot de passe",
-    "Save": "Enregistrer",
     "Save draft": "Enregistrer le brouillon",
-    "Search": "Rechercher",
+    "Save": "Enregistrer",
     "Search events, groups, etc.": "Rechercher des événements, des groupes, etc.",
-    "Search results: \"{search}\"": "Résultats de recherche : « {search} »",
+    "Search results: \"{search}\"": "Résultats de recherche : « {search} »",
+    "Search": "Rechercher",
     "Searching…": "Recherche en cours…",
     "Send me an email to reset my password": "Envoyez-moi un email pour réinitialiser mon mot de passe",
     "Send me the confirmation email once again": "Envoyez-moi l'email de confirmation encore une fois",
@@ -246,8 +248,8 @@
     "The draft event has been updated": "L'événement brouillon a été mis à jour",
     "The event has been created as a draft": "L'événement a été créé en tant que brouillon",
     "The event has been published": "L'événement a été publié",
-    "The event has been updated": "L'événement a été mis à jour",
     "The event has been updated and published": "L'événement a été mis à jour et publié",
+    "The event has been updated": "L'événement a été mis à jour",
     "The event organizer didn't add any description.": "L'organisateur de l'événement n'a pas ajouté de description.",
     "The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.",
     "The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
@@ -327,5 +329,6 @@
     "{count} participants": "Aucun⋅e participant⋅e | Un⋅e participant⋅e | {count} participant⋅e⋅s",
     "{count} requests waiting": "Une demande en attente|{count} demandes en attente",
     "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
-    "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines"
+    "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines",
+    "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
 }
diff --git a/js/src/types/address.model.ts b/js/src/types/address.model.ts
index 8db566691..d340e0db6 100644
--- a/js/src/types/address.model.ts
+++ b/js/src/types/address.model.ts
@@ -1,11 +1,14 @@
+import poiIcons from '@/utils/poiIcons';
+
 export interface IAddress {
-  id?: number;
+  id?: string;
   description: string;
   street: string;
   locality: string;
   postalCode: string;
   region: string;
   country: string;
+  type: string;
   geom?: string;
   url?: string;
   originId?: string;
@@ -18,4 +21,86 @@ export class Address implements IAddress {
   postalCode: string = '';
   region: string = '';
   street: string = '';
+  type: string = '';
+  id?: string = '';
+  originId?: string = '';
+  url?: string = '';
+  geom?: string = '';
+
+  constructor(hash?) {
+    if (!hash) return;
+
+    this.id = hash.id;
+    this.description = hash.description;
+    this.street = hash.street;
+    this.locality = hash.locality;
+    this.postalCode = hash.postalCode;
+    this.region = hash.region;
+    this.country = hash.country;
+    this.type = hash.type;
+    this.geom = hash.geom;
+    this.url = hash.url;
+    this.originId = hash.originId;
+  }
+
+  get poiInfos() {
+      /* generate name corresponding to poi type */
+    let name = '';
+    let alternativeName = '';
+    let poiIcon = poiIcons.default;
+    // Google Maps doesn't have a type
+    if (this.type == null && this.description === this.street) this.type = 'house';
+
+    switch (this.type) {
+      case 'house':
+        name = this.description;
+        alternativeName = [this.postalCode, this.locality, this.country].filter(zone => zone).join(', ');
+        poiIcon = poiIcons.defaultAddress;
+        break;
+      case 'street':
+      case 'secondary':
+        name = this.description;
+        alternativeName = [this.postalCode, this.locality, this.country].filter(zone => zone).join(', ');
+        poiIcon = poiIcons.defaultStreet;
+        break;
+      case 'zone':
+      case 'city':
+      case 'administrative':
+        name = this.postalCode ? `${this.description} (${this.postalCode})` : this.description;
+        alternativeName = [this.region, this.country].filter(zone => zone).join(', ');
+        poiIcon = poiIcons.defaultAdministrative;
+        break;
+      default:
+        // POI
+        name = this.description;
+        alternativeName = '';
+        if (this.street && this.street.trim()) {
+          alternativeName = `${this.street}`;
+          if (this.locality) {
+            alternativeName += ` (${this.locality})`;
+          }
+        } else if (this.locality && this.locality.trim()) {
+          alternativeName = `${this.locality}, ${this.region}, ${this.country}`;
+        } else {
+          alternativeName = `${this.region}, ${this.country}`;
+        }
+        poiIcon = this.iconForPOI;
+        break;
+    }
+    return { name, alternativeName, poiIcon };
+  }
+
+  get fullName() {
+    const { name, alternativeName } = this.poiInfos;
+    return `${name}, ${alternativeName}`;
+  }
+
+  get iconForPOI() {
+    if (this.type == null) {
+      return poiIcons.default;
+    }
+    const type = this.type.split(':').pop() || '';
+    if (poiIcons[type]) return poiIcons[type];
+    return poiIcons.default;
+  }
 }
diff --git a/js/src/types/config.model.ts b/js/src/types/config.model.ts
index 84f15eb6d..8da0a0c90 100644
--- a/js/src/types/config.model.ts
+++ b/js/src/types/config.model.ts
@@ -3,4 +3,10 @@ export interface IConfig {
   description: string;
 
   registrationsOpen: boolean;
+  countryCode: string;
+  location: {
+    latitude: number;
+    longitude: number;
+    accuracyRadius: number;
+  };
 }
diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts
index 494dbb45d..3c29374d1 100644
--- a/js/src/types/event.model.ts
+++ b/js/src/types/event.model.ts
@@ -1,5 +1,5 @@
 import { Actor, IActor, IPerson } from './actor';
-import { IAddress } from '@/types/address.model';
+import { Address, IAddress } from '@/types/address.model';
 import { ITag } from '@/types/tag.model';
 import { IPicture } from '@/types/picture.model';
 
@@ -239,7 +239,7 @@ export class EventModel implements IEvent {
 
     this.onlineAddress = hash.onlineAddress;
     this.phoneAddress = hash.phoneAddress;
-    this.physicalAddress = hash.physicalAddress;
+    this.physicalAddress = new Address(hash.physicalAddress);
     this.participantStats = hash.participantStats;
 
     this.tags = hash.tags;
diff --git a/js/src/utils/.editorconfig b/js/src/utils/.editorconfig
new file mode 100644
index 000000000..b6b82f05c
--- /dev/null
+++ b/js/src/utils/.editorconfig
@@ -0,0 +1,22 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 120
+tab_width = 4
+trim_trailing_whitespace = true
+
+[*.ex]
+indent_size = 2
+tab_width = 2
+
+[*.scss]
+indent_size = 2
+
+[*.ts]
+indent_size = 2
+tab_width = 2
diff --git a/js/src/utils/poiIcons.ts b/js/src/utils/poiIcons.ts
new file mode 100644
index 000000000..eb384aa0e
--- /dev/null
+++ b/js/src/utils/poiIcons.ts
@@ -0,0 +1,61 @@
+export default {
+  default: {
+    icon: 'map-marker',
+    color: '#5C6F84',
+  },
+  defaultAdministrative: {
+    icon: 'city',
+    color: '#5c6f84',
+  },
+  defaultStreet: {
+    icon: 'road-variant',
+    color: '#5c6f84',
+  },
+  defaultAddress: {
+    icon: 'home',
+    color: '#5c6f84',
+  },
+  place_house: {
+    icon: 'home',
+    color: '#5c6f84',
+  },
+  theatre: {
+    icon: 'drama-masks',
+  },
+  parking: {
+    icon: 'parking',
+  },
+  police: {
+    icon: 'police-badge',
+  },
+  post_office: {
+    icon: 'email',
+  },
+  university: {
+    icon: 'school',
+  },
+  college: {
+    icon: 'school',
+  },
+  park: {
+    icon: 'pine-tree',
+  },
+  garden: {
+    icon: 'pine-tree',
+  },
+  bicycle_rental: {
+    icon: 'bicycle',
+  },
+  hospital: {
+    icon: 'hospital-box',
+  },
+  townhall: {
+    icon: 'office-building',
+  },
+  toilets: {
+    icon: 'human-male-female',
+  },
+  hairdresser: {
+    icon: 'content-cut',
+  },
+};
diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue
index 465b3d8ec..50d2e0918 100644
--- a/js/src/views/Event/Event.vue
+++ b/js/src/views/Event/Event.vue
@@ -1,4 +1,3 @@
-import {ParticipantRole} from "@/types/event.model";
 <template>
   <div class="container">
     <b-loading :active.sync="$apollo.loading"></b-loading>
@@ -15,7 +14,7 @@ import {ParticipantRole} from "@/types/event.model";
                 <div class="title-and-informations">
                   <h1 class="title">{{ event.title }}</h1>
                   <span>
-                    <router-link v-if="actorIsOrganizer" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
+                    <router-link v-if="actorIsOrganizer && event.draft === false" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
                       <small v-if="event.participantStats.going > 0 && !actorIsParticipant">
                         {{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
                       </small>
@@ -111,23 +110,27 @@ import {ParticipantRole} from "@/types/event.model";
                   </p>
                 </div>
                 <div class="address-wrapper">
-                  <b-icon icon="map" />
-                  <span v-if="!event.physicalAddress">{{ $t('No address defined') }}</span>
-                  <div class="address" v-if="event.physicalAddress">
-                    <address>
-                      <span class="addressDescription" :title="event.physicalAddress.description">{{ event.physicalAddress.description }}</span>
-                      <span>{{ event.physicalAddress.street }}</span>
-                      <span>{{ event.physicalAddress.postalCode }} {{ event.physicalAddress.locality }}</span>
-                    </address>
-                    <span class="map-show-button" @click="showMap = !showMap" v-if="event.physicalAddress && event.physicalAddress.geom">
+                  <span v-if="!physicalAddress">
+                    <b-icon icon="map" />
+                    {{ $t('No address defined') }}
+                  </span>
+                  <div class="address" v-if="physicalAddress">
+                    <span>
+                      <b-icon :icon="physicalAddress.poiInfos.poiIcon.icon" />
+                      <address>
+                        <span class="addressDescription" :title="physicalAddress.poiInfos.name">{{ physicalAddress.poiInfos.name }}</span>
+                        <span>{{ physicalAddress.poiInfos.alternativeName }}</span>
+                      </address>
+                    </span>
+                    <span class="map-show-button" @click="showMap = !showMap" v-if="physicalAddress && physicalAddress.geom">
                       {{ $t('Show map') }}
                     </span>
                   </div>
-                  <b-modal v-if="event.physicalAddress && event.physicalAddress.geom" :active.sync="showMap" scroll="keep">
+                  <b-modal v-if="physicalAddress && physicalAddress.geom" :active.sync="showMap" scroll="keep">
                     <div class="map">
                       <map-leaflet
-                              :coords="event.physicalAddress.geom"
-                              :popup="event.physicalAddress.description"
+                              :coords="physicalAddress.geom"
+                              :marker="{ text: physicalAddress.fullName, icon: physicalAddress.poiInfos.poiIcon.icon }"
                       />
                     </div>
                   </b-modal>
@@ -254,7 +257,7 @@ import IdentityPicker from '@/views/Account/IdentityPicker.vue';
 import ParticipationButton from '@/components/Event/ParticipationButton.vue';
 import { GraphQLError } from 'graphql';
 import { RouteName } from '@/router';
-import HTML = Mocha.reporters.HTML;
+import { Address } from '@/types/address.model';
 
 @Component({
   components: {
@@ -596,11 +599,13 @@ export default class Event extends EventMixin {
   }
 
   get eventCapacityOK(): boolean {
+    if (this.event.draft) return true;
     if (!this.event.options.maximumAttendeeCapacity) return true;
     return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participant;
   }
 
   get numberOfPlacesStillAvailable(): number {
+    if (this.event.draft) return this.event.options.maximumAttendeeCapacity;
     return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participant;
   }
 
@@ -611,6 +616,11 @@ export default class Event extends EventMixin {
       return null;
     }
   }
+
+  get physicalAddress(): Address|null {
+    if (!this.event.physicalAddress) return null;
+    return new Address(this.event.physicalAddress);
+  }
 }
 </script>
 <style lang="scss" scoped>
@@ -664,25 +674,33 @@ export default class Event extends EventMixin {
           cursor: pointer;
         }
 
-        address {
-          font-style: normal;
-          flex-wrap: wrap;
+        span:first-child {
           display: flex;
-          justify-content: flex-start;
 
-          span.addressDescription {
-            text-overflow: ellipsis;
-            white-space: nowrap;
-            flex: 1 0 auto;
-            min-width: 100%;
-            max-width: 4rem;
-            overflow: hidden;
+          span.icon {
+            align-self: center;
           }
 
-          :not(.addressDescription) {
-            color: rgba(46, 62, 72, .6);
-            flex: 1;
-            min-width: 100%;
+          address {
+            font-style: normal;
+            flex-wrap: wrap;
+            display: flex;
+            justify-content: flex-start;
+
+            span.addressDescription {
+              text-overflow: ellipsis;
+              white-space: nowrap;
+              flex: 1 0 auto;
+              min-width: 100%;
+              max-width: 4rem;
+              overflow: hidden;
+            }
+
+            :not(.addressDescription) {
+              color: rgba(46, 62, 72, .6);
+              flex: 1;
+              min-width: 100%;
+            }
           }
         }
       }
diff --git a/js/src/vue-apollo.ts b/js/src/vue-apollo.ts
index 06ffa767b..10a045cf1 100644
--- a/js/src/vue-apollo.ts
+++ b/js/src/vue-apollo.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
 import { ApolloLink, Observable } from 'apollo-link';
-import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import { defaultDataIdFromObject, InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
 import { onError } from 'apollo-link-error';
 import { createLink } from 'apollo-absinthe-upload-link';
 import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
@@ -132,6 +132,13 @@ const link = authMiddleware
 
 const cache = new InMemoryCache({
   fragmentMatcher,
+  dataIdFromObject: object => {
+    if (object.__typename === 'Address') {
+      // @ts-ignore
+      return object.origin_id;
+    }
+    return defaultDataIdFromObject(object);
+  },
 });
 
 const apolloClient = new ApolloClient({
diff --git a/js/yarn.lock b/js/yarn.lock
index 3ea01ba69..9d93d1009 100644
--- a/js/yarn.lock
+++ b/js/yarn.lock
@@ -927,7 +927,14 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
   integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
 
-"@types/leaflet@^1.5.2":
+"@types/leaflet.locatecontrol@^0.60.7":
+  version "0.60.7"
+  resolved "https://registry.yarnpkg.com/@types/leaflet.locatecontrol/-/leaflet.locatecontrol-0.60.7.tgz#96d258bf27376b53bb4b3e9276a14e38f270215b"
+  integrity sha512-sac/MeK4gB+3XTJ3JzCe3HqLwKNHblIpZrxUJ6FapWK8uISZ0wcy8motVO7+v/yO47tQgsnYaobwFZ//beWHBQ==
+  dependencies:
+    "@types/leaflet" "*"
+
+"@types/leaflet@*", "@types/leaflet@^1.5.2":
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.5.tgz#006c0aa89c4b5e62941717fa71a09e846423536c"
   integrity sha512-Eyh1LMmW4OFgafL6rjLyGkMqFS5IzgwWHMSgTKbrsvwLjLaWH8Ae8CV5liRe8HSM731oOVDwAMIZgg9P0SO9tg==
@@ -7404,6 +7411,11 @@ lcid@^2.0.0:
   dependencies:
     invert-kv "^2.0.0"
 
+leaflet.locatecontrol@^0.68.0:
+  version "0.68.0"
+  resolved "https://registry.yarnpkg.com/leaflet.locatecontrol/-/leaflet.locatecontrol-0.68.0.tgz#fc0d173ef0f6670af192641e5a448f0c58c814d3"
+  integrity sha512-jXJCpBvkyH6shjPEOK/DWu/tKX/WdkNeO96jyPrnGelYp9u6wSDj4V1V4aX9+CMTIrEyVB4/4XuU+T7VTRpb6w==
+
 leaflet@^1.4.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.5.1.tgz#9afb9d963d66c870066b1342e7a06f92840f46bf"
diff --git a/lib/mobilizon/addresses/address.ex b/lib/mobilizon/addresses/address.ex
index 0b2de0930..ec04a749a 100644
--- a/lib/mobilizon/addresses/address.ex
+++ b/lib/mobilizon/addresses/address.ex
@@ -17,6 +17,7 @@ defmodule Mobilizon.Addresses.Address do
           geom: Geo.PostGIS.Geometry.t(),
           postal_code: String.t(),
           street: String.t(),
+          type: String.t(),
           url: String.t(),
           origin_id: String.t(),
           events: [Event.t()]
@@ -31,7 +32,8 @@ defmodule Mobilizon.Addresses.Address do
     :region,
     :postal_code,
     :street,
-    :origin_id
+    :origin_id,
+    :type
   ]
   @attrs @required_attrs ++ @optional_attrs
 
@@ -43,6 +45,7 @@ defmodule Mobilizon.Addresses.Address do
     field(:geom, Geo.PostGIS.Geometry)
     field(:postal_code, :string)
     field(:street, :string)
+    field(:type, :string)
     field(:url, :string)
     field(:origin_id, :string)
 
diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex
index 2978e785a..ce0b2b5ad 100644
--- a/lib/mobilizon/events/event.ex
+++ b/lib/mobilizon/events/event.ex
@@ -28,6 +28,7 @@ defmodule Mobilizon.Events.Event do
   alias Mobilizon.Media
   alias Mobilizon.Media.Picture
   alias Mobilizon.Mention
+  alias Mobilizon.Storage.Repo
 
   alias MobilizonWeb.Endpoint
   alias MobilizonWeb.Router.Helpers, as: Routes
@@ -105,7 +106,7 @@ defmodule Mobilizon.Events.Event do
     embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
     belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
     belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
-    belongs_to(:physical_address, Address, on_replace: :update)
+    belongs_to(:physical_address, Address, on_replace: :nilify)
     belongs_to(:picture, Picture, on_replace: :update)
     has_many(:tracks, Track)
     has_many(:sessions, Session)
@@ -194,11 +195,23 @@ defmodule Mobilizon.Events.Event do
         put_assoc(changeset, :physical_address, address)
 
       _ ->
-        changeset
+        cast_assoc(changeset, :physical_address)
     end
   end
 
-  # In case it's a new address
+  # In case it's a new address but the origin_id is an existing one
+  defp put_address(%Changeset{} = changeset, %{physical_address: %{origin_id: origin_id}})
+       when not is_nil(origin_id) do
+    case Repo.get_by(Address, origin_id: origin_id) do
+      %Address{} = address ->
+        put_assoc(changeset, :physical_address, address)
+
+      _ ->
+        cast_assoc(changeset, :physical_address)
+    end
+  end
+
+  # In case it's a new address without any origin_id (manual)
   defp put_address(%Changeset{} = changeset, _attrs) do
     cast_assoc(changeset, :physical_address)
   end
@@ -225,7 +238,7 @@ defmodule Mobilizon.Events.Event do
          %Changeset{changes: %{draft: true}} = changeset,
          _action
        ) do
-    cast_embed(changeset, :participant_stats)
+    put_embed(changeset, :participant_stats, %{creator: 0})
   end
 
   # Created with any other value: publish
diff --git a/lib/mobilizon_web/resolvers/address.ex b/lib/mobilizon_web/resolvers/address.ex
index 3754000a1..47d9f8475 100644
--- a/lib/mobilizon_web/resolvers/address.ex
+++ b/lib/mobilizon_web/resolvers/address.ex
@@ -3,7 +3,6 @@ defmodule MobilizonWeb.Resolvers.Address do
   Handles the comment-related GraphQL calls
   """
   require Logger
-  alias Mobilizon.Addresses
   alias Mobilizon.Addresses.Address
   alias Mobilizon.Service.Geospatial
 
@@ -11,26 +10,18 @@ defmodule MobilizonWeb.Resolvers.Address do
   Search an address
   """
   @spec search(map(), map(), map()) :: {:ok, list(Address.t())}
-  def search(_parent, %{query: query, page: _page, limit: _limit}, %{context: %{ip: ip}}) do
-    country = ip |> Geolix.lookup() |> Map.get(:country, nil)
+  def search(_parent, %{query: query, locale: locale, page: _page, limit: _limit}, %{
+        context: %{ip: ip}
+      }) do
+    geolix = Geolix.lookup(ip)
 
-    local_addresses = Task.async(fn -> Addresses.search_addresses(query, country: country) end)
+    country_code =
+      case geolix do
+        %{country: %{iso_code: country_code}} -> String.downcase(country_code)
+        _ -> nil
+      end
 
-    remote_addresses = Task.async(fn -> Geospatial.service().search(query) end)
-
-    addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
-
-    # If we have results with same origin_id than those locally saved, don't return them
-    addresses =
-      Enum.reduce(addresses, %{}, fn address, addresses ->
-        if Map.has_key?(addresses, address.origin_id) && !is_nil(address.url) do
-          addresses
-        else
-          Map.put(addresses, address.origin_id, address)
-        end
-      end)
-
-    addresses = Map.values(addresses)
+    addresses = Geospatial.service().search(query, lang: locale, country_code: country_code)
 
     {:ok, addresses}
   end
@@ -39,15 +30,12 @@ defmodule MobilizonWeb.Resolvers.Address do
   Reverse geocode some coordinates
   """
   @spec reverse_geocode(map(), map(), map()) :: {:ok, list(Address.t())}
-  def reverse_geocode(_parent, %{longitude: longitude, latitude: latitude}, %{context: %{ip: ip}}) do
-    country = ip |> Geolix.lookup() |> 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)
+  def reverse_geocode(
+        _parent,
+        %{longitude: longitude, latitude: latitude, zoom: zoom, locale: locale},
+        _context
+      ) do
+    addresses = Geospatial.service().geocode(longitude, latitude, lang: locale, zoom: zoom)
 
     {:ok, addresses}
   end
diff --git a/lib/mobilizon_web/resolvers/config.ex b/lib/mobilizon_web/resolvers/config.ex
index dc762c1e2..3f13c968b 100644
--- a/lib/mobilizon_web/resolvers/config.ex
+++ b/lib/mobilizon_web/resolvers/config.ex
@@ -4,16 +4,35 @@ defmodule MobilizonWeb.Resolvers.Config do
   """
 
   alias Mobilizon.Config
+  alias Geolix.Adapter.MMDB2.Record.{Country, Location}
 
   @doc """
   Gets config.
   """
-  def get_config(_parent, _params, _context) do
+  def get_config(_parent, _params, %{
+        context: %{ip: ip}
+      }) do
+    geolix = Geolix.lookup(ip)
+
+    country_code =
+      case geolix.city do
+        %{country: %Country{iso_code: country_code}} -> String.downcase(country_code)
+        _ -> nil
+      end
+
+    location =
+      case geolix.city do
+        %{location: %Location{} = location} -> Map.from_struct(location)
+        _ -> nil
+      end
+
     {:ok,
      %{
        name: Config.instance_name(),
        registrations_open: Config.instance_registrations_open?(),
-       description: Config.instance_description()
+       description: Config.instance_description(),
+       location: location,
+       country_code: country_code
      }}
   end
 end
diff --git a/lib/mobilizon_web/schema/address.ex b/lib/mobilizon_web/schema/address.ex
index da2dbc5ca..aea2a8156 100644
--- a/lib/mobilizon_web/schema/address.ex
+++ b/lib/mobilizon_web/schema/address.ex
@@ -13,6 +13,7 @@ defmodule MobilizonWeb.Schema.AddressType do
     field(:region, :string)
     field(:country, :string)
     field(:description, :string)
+    field(:type, :string)
     field(:url, :string)
     field(:id, :id)
     field(:origin_id, :string)
@@ -38,6 +39,7 @@ defmodule MobilizonWeb.Schema.AddressType do
     field(:country, :string)
     field(:description, :string)
     field(:url, :string)
+    field(:type, :string)
     field(:id, :id)
     field(:origin_id, :string)
   end
@@ -46,6 +48,7 @@ defmodule MobilizonWeb.Schema.AddressType do
     @desc "Search for an address"
     field :search_address, type: list_of(:address) do
       arg(:query, non_null(:string))
+      arg(:locale, :string, default_value: "en")
       arg(:page, :integer, default_value: 1)
       arg(:limit, :integer, default_value: 10)
 
@@ -56,6 +59,8 @@ defmodule MobilizonWeb.Schema.AddressType do
     field :reverse_geocode, type: list_of(:address) do
       arg(:longitude, non_null(:float))
       arg(:latitude, non_null(:float))
+      arg(:zoom, :integer, default_value: 15)
+      arg(:locale, :string, default_value: "en")
 
       resolve(&Resolvers.Address.reverse_geocode/3)
     end
diff --git a/lib/mobilizon_web/schema/config.ex b/lib/mobilizon_web/schema/config.ex
index e81f807d7..2b4dbecfb 100644
--- a/lib/mobilizon_web/schema/config.ex
+++ b/lib/mobilizon_web/schema/config.ex
@@ -13,6 +13,14 @@ defmodule MobilizonWeb.Schema.ConfigType do
     field(:description, :string)
 
     field(:registrations_open, :boolean)
+    field(:country_code, :string)
+    field(:location, :lonlat)
+  end
+
+  object :lonlat do
+    field(:longitude, :float)
+    field(:latitude, :float)
+    field(:accuracy_radius, :integer)
   end
 
   object :config_queries do
diff --git a/lib/service/geospatial/google_maps.ex b/lib/service/geospatial/google_maps.ex
index 1b2960d52..bf0454cf6 100644
--- a/lib/service/geospatial/google_maps.ex
+++ b/lib/service/geospatial/google_maps.ex
@@ -1,6 +1,6 @@
 defmodule Mobilizon.Service.Geospatial.GoogleMaps do
   @moduledoc """
-  Google Maps [Geocoding service](https://developers.google.com/maps/documentation/geocoding/intro).
+  Google Maps [Geocoding service](https://developers.google.com/maps/documentation/geocoding/intro). Only works with addresses.
 
   Note: Endpoint is hardcoded to Google Maps API.
   """
@@ -89,7 +89,11 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
           url <> "&address=#{args.q}"
 
         :geocode ->
-          url <> "&latlng=#{args.lat},#{args.lon}&result_type=street_address"
+          zoom = Keyword.get(options, :zoom, 15)
+
+          result_type = if zoom >= 15, do: "street_address", else: "locality"
+
+          url <> "&latlng=#{args.lat},#{args.lon}&result_type=#{result_type}"
 
         :place_details ->
           "https://maps.googleapis.com/maps/api/place/details/json?key=#{api_key}&placeid=#{
diff --git a/lib/service/geospatial/mimirsbrunn.ex b/lib/service/geospatial/mimirsbrunn.ex
new file mode 100644
index 000000000..333e9879a
--- /dev/null
+++ b/lib/service/geospatial/mimirsbrunn.ex
@@ -0,0 +1,146 @@
+defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
+  @moduledoc """
+  [Mimirsbrunn](https://github.com/CanalTP/mimirsbrunn) backend.
+
+  ## Issues
+    * Has trouble finding POIs.
+    * Doesn't support zoom level for reverse geocoding
+  """
+
+  alias Mobilizon.Addresses.Address
+  alias Mobilizon.Service.Geospatial.Provider
+  alias Mobilizon.Config
+
+  require Logger
+
+  @behaviour Provider
+
+  @endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
+
+  @impl Provider
+  @doc """
+  Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
+  """
+  @spec geocode(number(), number(), keyword()) :: list(Address.t())
+  def geocode(lon, lat, options \\ []) do
+    user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
+    headers = [{"User-Agent", user_agent}]
+    url = build_url(:geocode, %{lon: lon, lat: lat}, options)
+    Logger.debug("Asking Mimirsbrunn for reverse geocoding with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url, headers),
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      process_data(features)
+    end
+  end
+
+  @impl Provider
+  @doc """
+  Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
+  """
+  @spec search(String.t(), keyword()) :: list(Address.t())
+  def search(q, options \\ []) do
+    user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
+    headers = [{"User-Agent", user_agent}]
+    url = build_url(:search, %{q: q}, options)
+    Logger.debug("Asking Mimirsbrunn for addresses with #{url}")
+
+    with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+           HTTPoison.get(url, headers),
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      process_data(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}/autocomplete?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}"
+    end
+  end
+
+  defp process_data(features) do
+    features
+    |> Enum.map(fn %{
+                     "geometry" => %{"coordinates" => coordinates},
+                     "properties" => %{"geocoding" => geocoding}
+                   } ->
+      address = process_address(geocoding)
+      %Address{address | geom: Provider.coordinates(coordinates)}
+    end)
+  end
+
+  defp process_address(%{"type" => "poi", "address" => address} = geocoding) do
+    address = process_address(address)
+
+    %Address{
+      address
+      | type: get_type(geocoding),
+        origin_id: Map.get(geocoding, "id"),
+        description: Map.get(geocoding, "name")
+    }
+  end
+
+  defp process_address(geocoding) do
+    %Address{
+      country: get_administrative_region(geocoding, "country"),
+      locality: Map.get(geocoding, "city"),
+      region: get_administrative_region(geocoding, "region"),
+      description: Map.get(geocoding, "name"),
+      postal_code: get_postal_code(geocoding),
+      street: street_address(geocoding),
+      origin_id: "mimirsbrunn:" <> Map.get(geocoding, "id"),
+      type: get_type(geocoding)
+    }
+  end
+
+  defp street_address(properties) do
+    if Map.has_key?(properties, "housenumber") do
+      Map.get(properties, "housenumber") <> " " <> Map.get(properties, "street")
+    else
+      Map.get(properties, "street")
+    end
+  end
+
+  defp get_type(%{"type" => type}) when type in ["house", "street", "zone", "address"], do: type
+
+  defp get_type(%{"type" => "poi", "poi_types" => types})
+       when is_list(types) and length(types) > 0,
+       do: hd(types)["id"]
+
+  defp get_type(_), do: nil
+
+  defp get_administrative_region(
+         %{"administrative_regions" => administrative_regions},
+         administrative_level
+       ) do
+    Enum.find_value(
+      administrative_regions,
+      &process_administrative_region(&1, administrative_level)
+    )
+  end
+
+  defp get_administrative_region(_, _), do: nil
+
+  defp process_administrative_region(%{"zone_type" => "country", "name" => name}, "country"),
+    do: name
+
+  defp process_administrative_region(%{"zone_type" => "state", "name" => name}, "region"),
+    do: name
+
+  defp process_administrative_region(_, _), do: nil
+
+  defp get_postal_code(%{"postcode" => nil}), do: nil
+  defp get_postal_code(%{"postcode" => postcode}), do: postcode |> String.split(";") |> hd()
+end
diff --git a/lib/service/geospatial/nominatim.ex b/lib/service/geospatial/nominatim.ex
index 3f4a6b9c9..b956b1f19 100644
--- a/lib/service/geospatial/nominatim.ex
+++ b/lib/service/geospatial/nominatim.ex
@@ -27,8 +27,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
 
     with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
            HTTPoison.get(url, headers),
-         {:ok, body} <- Poison.decode(body) do
-      [process_data(body)]
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      features |> process_data() |> Enum.filter(& &1)
     end
   end
 
@@ -45,8 +45,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
 
     with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
            HTTPoison.get(url, headers),
-         {:ok, body} <- Poison.decode(body) do
-      body |> Enum.map(fn entry -> process_data(entry) end) |> Enum.filter(& &1)
+         {:ok, %{"features" => features}} <- Poison.decode(body) do
+      features |> process_data() |> Enum.filter(& &1)
     end
   end
 
@@ -55,39 +55,53 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
     limit = Keyword.get(options, :limit, 10)
     lang = Keyword.get(options, :lang, "en")
     endpoint = Keyword.get(options, :endpoint, @endpoint)
+    country_code = Keyword.get(options, :country_code)
+    zoom = Keyword.get(options, :zoom)
     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=#{
+          "#{endpoint}/search?format=geocodejson&q=#{URI.encode(args.q)}&limit=#{limit}&accept-language=#{
             lang
-          }&addressdetails=1"
+          }&addressdetails=1&namedetails=1"
 
         :geocode ->
-          "#{endpoint}/reverse?format=jsonv2&lat=#{args.lat}&lon=#{args.lon}&addressdetails=1"
+          url =
+            "#{endpoint}/reverse?format=geocodejson&lat=#{args.lat}&lon=#{args.lon}&accept-language=#{
+              lang
+            }&addressdetails=1&namedetails=1"
+
+          if is_nil(zoom), do: url, else: url <> "&zoom=#{zoom}"
       end
 
+    url = if is_nil(country_code), do: url, else: "#{url}&countrycodes=#{country_code}"
     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{
-      country: Map.get(address, "country"),
-      locality: Map.get(address, "city"),
-      region: Map.get(address, "state"),
-      description: description(body),
-      geom: [Map.get(body, "lon"), Map.get(body, "lat")] |> Provider.coordinates(),
-      postal_code: Map.get(address, "postcode"),
-      street: street_address(address),
-      origin_id: "osm:" <> to_string(Map.get(body, "osm_id"))
-    }
-  rescue
-    error in ArgumentError ->
-      Logger.warn(inspect(error))
+  defp process_data(features) do
+    features
+    |> Enum.map(fn %{
+                     "geometry" => %{"coordinates" => coordinates},
+                     "properties" => %{"geocoding" => geocoding}
+                   } ->
+      address = process_address(geocoding)
+      %Address{address | geom: Provider.coordinates(coordinates)}
+    end)
+  end
 
-      nil
+  defp process_address(geocoding) do
+    %Address{
+      country: Map.get(geocoding, "country"),
+      locality:
+        Map.get(geocoding, "city") || Map.get(geocoding, "town") || Map.get(geocoding, "county"),
+      region: Map.get(geocoding, "state"),
+      description: description(geocoding),
+      postal_code: Map.get(geocoding, "postcode"),
+      type: Map.get(geocoding, "type"),
+      street: street_address(geocoding),
+      origin_id: "nominatim:" <> to_string(Map.get(geocoding, "osm_id"))
+    }
   end
 
   @spec street_address(map()) :: String.t()
@@ -97,8 +111,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
         Map.has_key?(body, "road") ->
           Map.get(body, "road")
 
-        Map.has_key?(body, "road") ->
-          Map.get(body, "road")
+        Map.has_key?(body, "street") ->
+          Map.get(body, "street")
 
         Map.has_key?(body, "pedestrian") ->
           Map.get(body, "pedestrian")
@@ -107,7 +121,7 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
           ""
       end
 
-    Map.get(body, "house_number", "") <> " " <> road
+    Map.get(body, "housenumber", "") <> " " <> road
   end
 
   @address29_classes ["amenity", "shop", "tourism", "leisure"]
@@ -115,14 +129,16 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
 
   @spec description(map()) :: String.t()
   defp description(body) do
-    if !Map.has_key?(body, "display_name") do
-      Logger.warn("Address has no display name")
-      raise ArgumentError, message: "Address has no display_name"
-    end
-
-    description = Map.get(body, "display_name")
+    description = Map.get(body, "name")
     address = Map.get(body, "address")
 
+    description =
+      if Map.has_key?(body, "namedetails"),
+        do: body |> Map.get("namedetails") |> Map.get("name", description),
+        else: description
+
+    description = if is_nil(description), do: street_address(body), else: description
+
     if (Map.get(body, "category") in @address29_categories or
           Map.get(body, "class") in @address29_classes) and Map.has_key?(address, "address29") do
       Map.get(address, "address29")
diff --git a/lib/service/geospatial/provider.ex b/lib/service/geospatial/provider.ex
index e421df7f5..40d8b8278 100644
--- a/lib/service/geospatial/provider.ex
+++ b/lib/service/geospatial/provider.ex
@@ -16,6 +16,7 @@ defmodule Mobilizon.Service.Geospatial.Provider do
   * `: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"`.
+  * `:country_code` An ISO 3166 country code. String or `nil`
   * `: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
diff --git a/priv/repo/migrations/20191106141051_add_type_to_addresses.exs b/priv/repo/migrations/20191106141051_add_type_to_addresses.exs
new file mode 100644
index 000000000..cffee79cf
--- /dev/null
+++ b/priv/repo/migrations/20191106141051_add_type_to_addresses.exs
@@ -0,0 +1,9 @@
+defmodule Mobilizon.Storage.Repo.Migrations.AddTypeToAddresses do
+  use Ecto.Migration
+
+  def change do
+    alter table(:addresses) do
+      add(:type, :string)
+    end
+  end
+end
diff --git a/schema.graphql b/schema.graphql
index ec149f147..1dea5d5cd 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -1,5 +1,5 @@
 # source: http://localhost:4000/api
-# timestamp: Wed Nov 06 2019 12:50:45 GMT+0100 (Central European Standard Time)
+# timestamp: Fri Nov 08 2019 17:20:47 GMT+0100 (Central European Standard Time)
 
 schema {
   query: RootQueryType
@@ -131,6 +131,7 @@ type Address {
 
   """The address's street name (with number)"""
   street: String
+  type: String
   url: String
 }
 
@@ -150,6 +151,7 @@ input AddressInput {
 
   """The address's street name (with number)"""
   street: String
+  type: String
   url: String
 }
 
@@ -187,7 +189,9 @@ enum CommentVisibility {
 
 """A config object"""
 type Config {
+  countryCode: String
   description: String
+  location: Lonlat
   name: String
   registrationsOpen: Boolean
 }
@@ -629,6 +633,12 @@ type Login {
   user: User!
 }
 
+type Lonlat {
+  accuracyRadius: Int
+  latitude: Float
+  longitude: Float
+}
+
 """
 Represents a member of a group
 
@@ -1171,7 +1181,7 @@ type RootQueryType {
   reverseGeocode(latitude: Float!, longitude: Float!): [Address]
 
   """Search for an address"""
-  searchAddress(limit: Int = 10, page: Int = 1, query: String!): [Address]
+  searchAddress(limit: Int = 10, locale: String = "en", page: Int = 1, query: String!): [Address]
 
   """Search events"""
   searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
index d8513a89d..ad9beef81 100644
--- a/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
+++ b/test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
@@ -2,17 +2,19 @@
   {
     "request": {
       "body": "",
-      "headers": [],
+      "headers": {
+        "User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1"
+      },
       "method": "get",
       "options": [],
       "request_body": "",
-      "url": "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=45.751718&lon=4.842569&addressdetails=1"
+      "url": "https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat=45.751718&lon=4.842569&accept-language=en&addressdetails=1&namedetails=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\"]}",
+      "body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"45.751718,4.842569\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"accuracy\":0,\"label\":\"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\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}",
       "headers": {
-        "Date": "Thu, 14 Mar 2019 10:26:11 GMT",
+        "Date": "Tue, 12 Nov 2019 12:21:45 GMT",
         "Server": "Apache/2.4.29 (Ubuntu)",
         "Access-Control-Allow-Origin": "*",
         "Access-Control-Allow-Methods": "OPTIONS,GET",
diff --git a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json b/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
index c41b678cb..395cf11ee 100644
--- a/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
+++ b/test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
@@ -2,17 +2,19 @@
   {
     "request": {
       "body": "",
-      "headers": [],
+      "headers": {
+        "User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1"
+      },
       "method": "get",
       "options": [],
       "request_body": "",
-      "url": "https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1"
+      "url": "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1&namedetails=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\"}}]",
+      "body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"10 rue Jangot\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"label\":\"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\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}",
       "headers": {
-        "Date": "Thu, 14 Mar 2019 10:24:24 GMT",
+        "Date": "Tue, 12 Nov 2019 12:21:46 GMT",
         "Server": "Apache/2.4.29 (Ubuntu)",
         "Access-Control-Allow-Origin": "*",
         "Access-Control-Allow-Methods": "OPTIONS,GET",
diff --git a/test/mobilizon/service/geospatial/nominatim_test.exs b/test/mobilizon/service/geospatial/nominatim_test.exs
index 04bec7da4..b64bcb935 100644
--- a/test/mobilizon/service/geospatial/nominatim_test.exs
+++ b/test/mobilizon/service/geospatial/nominatim_test.exs
@@ -29,7 +29,7 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do
 
         assert_called(
           HTTPoison.get(
-            "https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1",
+            "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1&namedetails=1",
             @httpoison_headers
           )
         )
@@ -38,43 +38,46 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do
 
     test "returns a valid address from search" do
       use_cassette "geospatial/nominatim/search" do
-        assert %Address{
-                 locality: "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",
-                 region: "Auvergne-Rhône-Alpes",
-                 country: "France",
-                 postal_code: "69007",
-                 street: "10 Rue Jangot",
-                 geom: %Geo.Point{
-                   coordinates: {4.8425657, 45.7517141},
-                   properties: %{},
-                   srid: 4326
-                 },
-                 origin_id: "osm:3078260611"
-               } == Nominatim.search("10 rue Jangot") |> hd
+        assert [
+                 %Address{
+                   locality: "Lyon",
+                   description: "10 Rue Jangot",
+                   region: "Auvergne-Rhône-Alpes",
+                   country: "France",
+                   postal_code: "69007",
+                   street: "10 Rue Jangot",
+                   geom: %Geo.Point{
+                     coordinates: {4.8425657, 45.7517141},
+                     properties: %{},
+                     srid: 4326
+                   },
+                   origin_id: "nominatim:3078260611",
+                   type: "house"
+                 }
+               ] == Nominatim.search("10 rue Jangot")
       end
     end
 
     test "returns a valid address from reverse geocode" do
       use_cassette "geospatial/nominatim/geocode" do
-        assert %Address{
-                 locality: "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",
-                 region: "Auvergne-Rhône-Alpes",
-                 country: "France",
-                 postal_code: "69007",
-                 street: "10 Rue Jangot",
-                 geom: %Geo.Point{
-                   coordinates: {4.8425657, 45.7517141},
-                   properties: %{},
-                   srid: 4326
-                 },
-                 origin_id: "osm:3078260611"
-               } ==
+        assert [
+                 %Address{
+                   locality: "Lyon",
+                   description: "10 Rue Jangot",
+                   region: "Auvergne-Rhône-Alpes",
+                   country: "France",
+                   postal_code: "69007",
+                   street: "10 Rue Jangot",
+                   geom: %Geo.Point{
+                     coordinates: {4.8425657, 45.7517141},
+                     properties: %{},
+                     srid: 4326
+                   },
+                   origin_id: "nominatim:3078260611",
+                   type: "house"
+                 }
+               ] ==
                  Nominatim.geocode(4.842569, 45.751718)
-                 |> hd
       end
     end
   end
diff --git a/test/mobilizon_web/resolvers/address_resolver_test.exs b/test/mobilizon_web/resolvers/address_resolver_test.exs
index a97d36ec4..47a944c3b 100644
--- a/test/mobilizon_web/resolvers/address_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/address_resolver_test.exs
@@ -26,11 +26,6 @@ defmodule MobilizonWeb.Resolvers.AddressResolverTest do
     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) {
@@ -44,7 +39,8 @@ defmodule MobilizonWeb.Resolvers.AddressResolverTest do
         conn
         |> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
 
-      assert json_response(res, 200)["data"]["reverseGeocode"] == []
+      assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
+               "Anywhere"
 
       query = """
         {
@@ -60,7 +56,7 @@ defmodule MobilizonWeb.Resolvers.AddressResolverTest do
         |> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
 
       assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
-               address.description
+               "10 rue Jangot, Lyon"
     end
   end
 end
diff --git a/test/support/mocks/geospatial_mock.ex b/test/support/mocks/geospatial_mock.ex
index 870047466..f0ed1aea2 100644
--- a/test/support/mocks/geospatial_mock.ex
+++ b/test/support/mocks/geospatial_mock.ex
@@ -9,7 +9,9 @@ defmodule Mobilizon.Service.Geospatial.Mock do
   @behaviour Provider
 
   @impl Provider
-  def geocode(_lon, _lat, _options \\ []), do: []
+  def geocode(_lon, _lat, _options \\ [])
+  def geocode(45.75, 4.85, _options), do: [%Address{description: "10 rue Jangot, Lyon"}]
+  def geocode(_lon, _lat, _options), do: [%Address{description: "Anywhere"}]
 
   @impl Provider
   def search(_q, _options \\ []), do: [%Address{description: "10 rue Jangot, Lyon"}]