From 4e9f1139ec16fe17a86ab4c81f26868165ced5b7 Mon Sep 17 00:00:00 2001
From: Julian Nacci <julian.nacci@gmail.com>
Date: Mon, 28 Nov 2016 19:37:07 +0100
Subject: [PATCH] Add new lookup: ban_data_gouv_fr
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

add new lookup to lookup.rb

add new lookup file ban_data_gouv_fr.rb

add new results file ban_data_gouv_fr.rb

update README with new lookup

create fixtures to test  new lookup

create unit test file for new lookup

update test helper default fixture for new lookup

remove invalid multibyte char (US-ASCII) 'ç' from lookup name

Revert "remove invalid multibyte char (US-ASCII) 'ç' from lookup name"

This reverts commit 7dbfcab4dc4bcb9473edc43651e7d53884f0e24d.

Add '# encoding: utf-8' to files containing special chars

Add '# encoding: utf-8' to files containing special chars

Add '# encoding: utf-8' to files containing special chars

remove useless Geocoder.config from test methods

refacto of ban lookup results method

change city serach param to fit naming conventions in ban lookup test

renamed no results ban lookup fixture to fit tests

removed no search results unnecessary test

remove useless commented code

add alias method to handle state and state_code + move from department to region name as state

refacto results method
---
 README.md                                     |  10 +
 lib/geocoder/lookup.rb                        |   1 +
 lib/geocoder/lookups/ban_data_gouv_fr.rb      | 130 +++++++++
 lib/geocoder/results/ban_data_gouv_fr.rb      | 257 ++++++++++++++++++
 test/fixtures/ban_data_gouv_fr_montpellier    | 125 +++++++++
 test/fixtures/ban_data_gouv_fr_no_results     |   9 +
 .../ban_data_gouv_fr_no_reverse_results       |   8 +
 test/fixtures/ban_data_gouv_fr_paris          | 117 ++++++++
 test/fixtures/ban_data_gouv_fr_reverse        |  33 +++
 .../fixtures/ban_data_gouv_fr_rue_yves_toudic |  33 +++
 test/test_helper.rb                           |  13 +
 test/unit/lookups/ban_data_gouv_fr_test.rb    | 135 +++++++++
 12 files changed, 871 insertions(+)
 create mode 100644 lib/geocoder/lookups/ban_data_gouv_fr.rb
 create mode 100644 lib/geocoder/results/ban_data_gouv_fr.rb
 create mode 100644 test/fixtures/ban_data_gouv_fr_montpellier
 create mode 100644 test/fixtures/ban_data_gouv_fr_no_results
 create mode 100644 test/fixtures/ban_data_gouv_fr_no_reverse_results
 create mode 100644 test/fixtures/ban_data_gouv_fr_paris
 create mode 100644 test/fixtures/ban_data_gouv_fr_reverse
 create mode 100644 test/fixtures/ban_data_gouv_fr_rue_yves_toudic
 create mode 100644 test/unit/lookups/ban_data_gouv_fr_test.rb

diff --git a/README.md b/README.md
index 5deea9a8..06dde602 100644
--- a/README.md
+++ b/README.md
@@ -696,6 +696,16 @@ This uses the PostcodeAnywhere UK Geocode service, this will geocode any string
 * **Terms of Service**: ?
 * **Limitations**: No restrictions on use
 
+#### Base Adresse Nationale FR (`:ban_data_gouv_fr`)
+
+* **API key**: none
+* **Quota**: none
+* **Region**: FR
+* **SSL support**: yes
+* **Languages**: en / fr
+* **Documentation**: https://adresse.data.gouv.fr/api/ (in french)
+* **Terms of Service**: https://adresse.data.gouv.fr/faq/ (in french)
+* **Limitations**: [Data licensed under Open Database License (ODbL) (you must provide attribution).](http://openstreetmap.fr/ban)
 
 ### IP Address Services
 
diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb
index b704d7cd..b5e9275a 100644
--- a/lib/geocoder/lookup.rb
+++ b/lib/geocoder/lookup.rb
@@ -47,6 +47,7 @@ module Geocoder
         :okf,
         :postcode_anywhere_uk,
         :geoportail_lu,
+        :ban_data_gouv_fr,
         :test,
         :latlon
       ]
diff --git a/lib/geocoder/lookups/ban_data_gouv_fr.rb b/lib/geocoder/lookups/ban_data_gouv_fr.rb
new file mode 100644
index 00000000..1aad701f
--- /dev/null
+++ b/lib/geocoder/lookups/ban_data_gouv_fr.rb
@@ -0,0 +1,130 @@
+# encoding: utf-8
+
+require 'geocoder/lookups/base'
+require 'geocoder/results/ban_data_gouv_fr'
+
+module Geocoder::Lookup
+  class BanDataGouvFr < Base
+
+    def name
+      "Base Adresse Nationale Française"
+    end
+
+    def map_link_url(coordinates)
+      "https://www.openstreetmap.org/#map=19/#{coordinates.join('/')}"
+    end
+
+    def query_url(query)
+      method = query.reverse_geocode? ? "reverse" : "search"
+      "#{protocol}://api-adresse.data.gouv.fr/#{method}/?" + url_query_string(query)
+    end
+
+    private # ---------------------------------------------------------------
+
+    def any_result?(query)
+      fetch_data(query)['features'].any?
+    end
+
+    def results(query)
+      if doc = fetch_data(query) and any_result?(doc)
+        [doc]
+      else
+        []
+      end
+    end
+
+    #### PARAMS ####
+
+    def query_url_params(query)
+      query_ban_datagouv_fr_params(query).merge(super)
+    end
+
+    def query_ban_datagouv_fr_params(query)
+      query.reverse_geocode? ? reverse_geocode_ban_fr_params(query) : search_geocode_ban_fr_params(query)
+    end
+
+    #### SEARCH GEOCODING PARAMS ####
+    #
+    #  :q             =>    required, full text search param)
+
+    #  :limit         =>    force limit number of results returned by raw API
+    #                       (default = 5) note : only first result is taken
+    #                       in account in geocoder
+    #
+    #  :autocomplete  =>    pass 0 to disable autocomplete treatment of :q
+    #                       (default = 1)
+    #
+    #  :lat           =>    force filter results around specific lat/lon
+    #
+    #  :lon           =>    force filter results around specific lat/lon
+    #
+    #  :type          =>    force filter the returned result type
+    #                       (check results for a list of accepted types)
+    #
+    #  :postcode      =>    force filter results on a specific city post code
+    #
+    #  :citycode      =>    force filter results on a specific city UUID INSEE code
+    #
+    #  For up to date doc (in french only) : https://adresse.data.gouv.fr/api/
+    #
+    def search_geocode_ban_fr_params(query)
+      params = {
+        q: query.sanitized_text
+      }
+      unless (limit = query.options[:limit]).nil? || !limit_param_is_valid?(limit)
+        params[:limit] = limit.to_i
+      end
+      unless (autocomplete = query.options[:autocomplete]).nil? || !autocomplete_param_is_valid?(autocomplete)
+        params[:autocomplete] = autocomplete.to_s
+      end
+      unless (type = query.options[:type]).nil? || !type_param_is_valid?(type)
+        params[:type] = type.downcase
+      end
+      unless (postcode = query.options[:postcode]).nil? || !code_param_is_valid?(postcode)
+        params[:postcode] = postcode.to_s
+      end
+      unless (citycode = query.options[:citycode]).nil? || !code_param_is_valid?(citycode)
+        params[:citycode] = citycode.to_s
+      end
+      params
+    end
+
+    #### REVERSE GEOCODING PARAMS ####
+    #
+    #  :lat           =>    required
+    #
+    #  :lon           =>    required
+    #
+    #  :type          =>    force returned results type
+    #                       (check results for a list of accepted types)
+    #
+    def reverse_geocode_ban_fr_params(query)
+      lat_lon = query.coordinates
+      params = {
+          lat: lat_lon.first,
+          lon: lat_lon.last
+      }
+      unless (type = query.options[:type]).nil? || !type_param_is_valid?(type)
+        params[:type] = type.downcase
+      end
+      params
+    end
+
+    def limit_param_is_valid?(param)
+      param.to_i.positive?
+    end
+
+    def autocomplete_param_is_valid?(param)
+      [0,1].include?(param.to_i)
+    end
+
+    def type_param_is_valid?(param)
+      %w(housenumber street locality village town city).include?(param.downcase)
+    end
+
+    def code_param_is_valid?(param)
+      (1..99999).include?(param.to_i)
+    end
+
+  end
+end
diff --git a/lib/geocoder/results/ban_data_gouv_fr.rb b/lib/geocoder/results/ban_data_gouv_fr.rb
new file mode 100644
index 00000000..0b936b04
--- /dev/null
+++ b/lib/geocoder/results/ban_data_gouv_fr.rb
@@ -0,0 +1,257 @@
+# encoding: utf-8
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class BanDataGouvFr < Base
+
+    #### BASE METHODS ####
+
+    def self.response_attributes
+      %w[limit attribution version licence type features]
+    end
+
+    response_attributes.each do |a|
+      unless method_defined?(a)
+        define_method a do
+          @data[a]
+        end
+      end
+    end
+
+    #### BEST RESULT ####
+
+    def result
+      features[0] if features.any?
+    end
+
+    #### GEOMETRY ####
+
+    def geometry
+      result['geometry'] if result
+    end
+
+    def precision
+      geometry['type'] if geometry
+    end
+
+    def coordinates
+      coords = geometry["coordinates"]
+      return [coords[1].to_f, coords[0].to_f]
+    end
+
+    #### PROPERTIES ####
+
+    # List of raw attrbutes returned by BAN data gouv fr API:
+    #
+    #   :id           =>    [string] UUID of the result, said to be not stable
+    #                       atm, based on IGN reference (Institut national de
+    #                       l'information géographique et forestière)
+    #
+    #   :type         =>    [string] result type (housenumber, street, city,
+    #                       town, village, locality)
+    #
+    #   :score        =>    [float] value between 0 and 1 giving result's
+    #                       relevancy
+    #
+    #   :housenumber  =>    [string] street number and extra information
+    #                       (bis, ter, A, B)
+    #
+    #   :street       =>    [string] street name
+    #
+    #   :name         =>    [string] housenumber and street name
+    #
+    #   :postcode     =>    [string] city post code (used for mails by La Poste,
+    #                       beware many cities got severeal postcodes)
+    #
+    #   :citycode     =>    [string] city code (INSEE reference,
+    #                       consider it as a french institutional UUID)
+    #
+    #   :city         =>    [string] city name
+    #
+    #   :context      =>    [string] department code, department name and
+    #                       region code
+    #
+    #   :label        =>    [string] full address without state, country name
+    #                       and country code
+    #
+    # CITIES ONLY PROPERTIES
+    #
+    #   :adm_weight   =>    [string] administrative weight (importance) of
+    #                       the city
+    #
+    #   :population   =>    [float] number of inhabitants with a 1000 factor
+    #
+    # For up to date doc (in french only) : https://adresse.data.gouv.fr/api/
+    #
+    def properties
+      result['properties'] if result
+    end
+
+    # List of usable Geocoder results' methods
+    #
+    #   score                   =>    [float] result relevance 0 to 1
+    #
+    #   location_id             =>    [string] location's IGN UUID
+    #
+    #   result_type             =>    [string] housenumber / street / city
+    #                                 / town / village / locality
+    #
+    #   international_address   =>    [string] full address with country code
+    #
+    #   national_address        =>    [string] full address with country code
+    #
+    #   street_address          =>    [string] housenumber + extra inf
+    #                                 + street name
+    #
+    #   street_number           =>    [string] housenumber + extra inf
+    #                                 (bis, ter, etc)
+    #
+    #   street_name             =>    [string] street's name
+    #
+    #   city_name               =>    [string] city's name
+    #
+    #   city_code               =>    [string] city's INSEE UUID
+    #
+    #   postal_code             =>    [string] city's postal code (used for mails)
+    #
+    #   context                 =>    [string] city's department code, department
+    #                                  name and region name
+    #
+    #   demartment_name         =>    [string] city's department name
+    #
+    #   department_code         =>    [string] city's department INSEE UUID
+    #
+    #   region_name             =>    [string] city's region name
+    #
+    #   population              =>    [string] city's inhabitants count
+    #
+    #   administrative_weight   =>    [integer] city's importance on a scale
+    #                                 from 6 (capital city) to 1 (regular village)
+    #
+    def score
+      properties['score']
+    end
+
+    def location_id
+      properties['id']
+    end
+
+    # Types
+    #
+    #   housenumber
+    #   street
+    #   city
+    #   town
+    #   village
+    #   locality
+    #
+    def result_type
+      properties['type']
+    end
+
+    def international_address
+      "#{national_address}, #{country}"
+    end
+
+    def national_address
+      properties['label']
+    end
+
+    def street_address
+      properties['name']
+    end
+
+    def street_number
+      properties['housenumber']
+    end
+
+    def street_name
+      properties['street']
+    end
+
+    def city_name
+      properties['city']
+    end
+
+    def city_code
+      properties['citycode']
+    end
+
+    def postal_code
+      properties['postcode']
+    end
+
+    def context
+      properties['context'].split(/,/).map(&:strip)
+    end
+
+    def department_code
+      context[0] if context.length > 0
+    end
+
+    # Monkey logic to handle fact Paris is both a city and a department
+    # in ÃŽle-de-France region
+    def department_name
+      if context.length > 1
+        if context[1] == "ÃŽle-de-France"
+          "Paris"
+        else
+          context[1]
+        end
+      end
+    end
+
+    def region_name
+      if context.length == 2 && context[1] == "ÃŽle-de-France"
+        context[1]
+      elsif context.length > 2
+        context[2]
+      end
+    end
+
+    def country
+      "France"
+    end
+
+    # Country code types
+    #    FR : France
+    #    GF : Guyane Française
+    #    RE : Réunion
+    #    NC : Nouvelle-Calédonie
+    #    GP : Guadeloupe
+    #    MQ : Martinique
+    #    MU : Maurice
+    #    PF : Polynésie française
+    #
+    # Will need refacto to handle different country codes, but BAN API
+    # is currently mainly designed for geocode FR country code addresses
+    def country_code
+      "FR"
+    end
+
+    #### ALIAS METHODS ####
+
+    alias_method :address, :international_address
+    alias_method :street, :street_name
+    alias_method :city, :city_name
+    alias_method :state, :region_name
+    alias_method :state_code, :state
+
+    #### CITIES' METHODS ####
+
+    def population
+      (properties['population'].to_f * 1000).to_i if city?(result_type)
+    end
+
+    def administrative_weight
+      properties['adm_weight'].to_i if city?(result_type)
+    end
+
+    private
+
+    def city?(result_type)
+      %w(village town city).include?(result_type)
+    end
+
+  end
+end
diff --git a/test/fixtures/ban_data_gouv_fr_montpellier b/test/fixtures/ban_data_gouv_fr_montpellier
new file mode 100644
index 00000000..5000353a
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_montpellier
@@ -0,0 +1,125 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "Montpellier",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34080",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172_34080",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34000",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34090",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172_34090",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.875521,
+          43.611024
+        ]
+      },
+      "properties": {
+        "citycode": "34172",
+        "adm_weight": "5",
+        "name": "Montpellier",
+        "city": "Montpellier",
+        "postcode": "34070",
+        "context": "34, Hérault, Languedoc-Roussillon",
+        "score": 0.9785090909090908,
+        "label": "Montpellier",
+        "id": "34172_34070",
+        "type": "city",
+        "population": "255.1"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          -0.744328,
+          45.634502
+        ]
+      },
+      "properties": {
+        "citycode": "17244",
+        "adm_weight": "1",
+        "name": "Montpellier-de-Médillan",
+        "city": "Montpellier-de-Médillan",
+        "postcode": "17260",
+        "context": "17, Charente-Maritime, Poitou-Charentes",
+        "score": 0.825,
+        "label": "Montpellier-de-Médillan",
+        "id": "17244",
+        "type": "village",
+        "population": "0.6"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/ban_data_gouv_fr_no_results b/test/fixtures/ban_data_gouv_fr_no_results
new file mode 100644
index 00000000..3c2e7833
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_no_results
@@ -0,0 +1,9 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "oozpuip",
+  "type": "FeatureCollection",
+  "features": []
+}
diff --git a/test/fixtures/ban_data_gouv_fr_no_reverse_results b/test/fixtures/ban_data_gouv_fr_no_reverse_results
new file mode 100644
index 00000000..657d238f
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_no_reverse_results
@@ -0,0 +1,8 @@
+{
+  "limit": 1,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "type": "FeatureCollection",
+  "features": []
+}
diff --git a/test/fixtures/ban_data_gouv_fr_paris b/test/fixtures/ban_data_gouv_fr_paris
new file mode 100644
index 00000000..a1e46a09
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_paris
@@ -0,0 +1,117 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "Paris",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.3469,
+          48.8589
+        ]
+      },
+      "properties": {
+        "adm_weight": "6",
+        "citycode": "75056",
+        "name": "Paris",
+        "city": "Paris",
+        "postcode": "75000",
+        "context": "75, ÃŽle-de-France",
+        "score": 1,
+        "label": "Paris",
+        "id": "75056",
+        "type": "city",
+        "population": "2244"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          3.564293,
+          45.766413
+        ]
+      },
+      "properties": {
+        "citycode": "63125",
+        "postcode": "63120",
+        "name": "Paris",
+        "city": "Courpière",
+        "context": "63, Puy-de-Dôme, Auvergne",
+        "score": 0.8255363636363636,
+        "label": "Paris 63120 Courpière",
+        "id": "63125_B221_03549b",
+        "type": "locality"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          1.550208,
+          44.673592
+        ]
+      },
+      "properties": {
+        "citycode": "46138",
+        "postcode": "46240",
+        "name": "PARIS (Vaillac)",
+        "city": "Cœur de Causse",
+        "context": "46, Lot, Midi-Pyrénées",
+        "score": 0.824090909090909,
+        "label": "PARIS (Vaillac) 46240 Cœur de Causse",
+        "id": "46138_XXXX_6ee4ec",
+        "type": "street"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          -0.526884,
+          43.762253
+        ]
+      },
+      "properties": {
+        "citycode": "40282",
+        "postcode": "40500",
+        "name": "Paris",
+        "city": "Saint-Sever",
+        "context": "40, Landes, Aquitaine",
+        "score": 0.8236181818181818,
+        "label": "Paris 40500 Saint-Sever",
+        "id": "40282_B237_2364e3",
+        "type": "locality"
+      },
+      "type": "Feature"
+    },
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          0.157613,
+          47.336685
+        ]
+      },
+      "properties": {
+        "citycode": "37031",
+        "postcode": "37140",
+        "name": "Paris Buton",
+        "city": "Bourgueil",
+        "context": "37, Indre-et-Loire, Centre Val-de-Loire",
+        "score": 0.8235454545454545,
+        "label": "Paris Buton 37140 Bourgueil",
+        "id": "37031_B165_0a5e7a",
+        "type": "locality"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/ban_data_gouv_fr_reverse b/test/fixtures/ban_data_gouv_fr_reverse
new file mode 100644
index 00000000..b3a1b680
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_reverse
@@ -0,0 +1,33 @@
+{
+  "limit": 1,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.364375,
+          48.770639
+        ]
+      },
+      "properties": {
+        "street": "Rue du Lieutenant Alain le Coz",
+        "label": "4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue",
+        "distance": 23,
+        "context": "94, Val-de-Marne, ÃŽle-de-France",
+        "id": "94021_1133_49638b",
+        "citycode": "94021",
+        "name": "4 Rue du Lieutenant Alain le Coz",
+        "city": "Chevilly-Larue",
+        "postcode": "94550",
+        "housenumber": "4",
+        "score": 0.9999997696809948,
+        "type": "housenumber"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/fixtures/ban_data_gouv_fr_rue_yves_toudic b/test/fixtures/ban_data_gouv_fr_rue_yves_toudic
new file mode 100644
index 00000000..dea2d42c
--- /dev/null
+++ b/test/fixtures/ban_data_gouv_fr_rue_yves_toudic
@@ -0,0 +1,33 @@
+{
+  "limit": 5,
+  "attribution": "BAN",
+  "version": "draft",
+  "licence": "ODbL 1.0",
+  "query": "13 rue yves toudic 75010 Paris",
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.363473,
+          48.870131
+        ]
+      },
+      "properties": {
+        "citycode": "75110",
+        "postcode": "75010",
+        "name": "13 Rue Yves Toudic",
+        "housenumber": "13",
+        "city": "Paris",
+        "context": "75, ÃŽle-de-France",
+        "score": 0.9437454545454544,
+        "label": "13 Rue Yves Toudic 75010 Paris",
+        "id": "ADRNIVX_0000000270748760",
+        "type": "housenumber",
+        "street": "Rue Yves Toudic"
+      },
+      "type": "Feature"
+    }
+  ]
+}
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 54a580dc..f2e6f275 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -361,6 +361,19 @@ module Geocoder
       end
     end
 
+    require 'geocoder/lookups/ban_data_gouv_fr'
+    class BanDataGouvFr
+      private
+      def fixture_prefix
+        "ban_data_gouv_fr"
+      end
+
+      def default_fixture_filename
+        "#{fixture_prefix}_rue_yves_toudic"
+      end
+    end
+
+
   end
 end
 
diff --git a/test/unit/lookups/ban_data_gouv_fr_test.rb b/test/unit/lookups/ban_data_gouv_fr_test.rb
new file mode 100644
index 00000000..8229c988
--- /dev/null
+++ b/test/unit/lookups/ban_data_gouv_fr_test.rb
@@ -0,0 +1,135 @@
+# encoding: utf-8
+require 'test_helper'
+
+class BanDataGouvFrTest < GeocoderTestCase
+
+  def setup
+    Geocoder.configure(lookup: :ban_data_gouv_fr)
+  end
+
+  def test_query_for_geocode
+    query = Geocoder::Query.new('13 rue yves toudic, 75010 Paris')
+    lookup = Geocoder::Lookup.get(:ban_data_gouv_fr)
+    res = lookup.query_url(query)
+    assert_equal 'https://api-adresse.data.gouv.fr/search/?q=13+rue+yves+toudic%2C+75010+Paris', res
+  end
+
+  def test_query_for_reverse_geocode
+    query = Geocoder::Query.new([48.770639, 2.364375])
+    lookup = Geocoder::Lookup.get(:ban_data_gouv_fr)
+    res = lookup.query_url(query)
+    assert_equal 'https://api-adresse.data.gouv.fr/reverse/?lat=48.770639&lon=2.364375', res
+  end
+
+  def test_results_component
+    result = Geocoder.search('13 rue yves toudic, 75010 Paris').first
+    assert_equal 'ADRNIVX_0000000270748760', result.location_id
+    assert_equal 'housenumber', result.result_type
+    assert_equal 'Paris', result.city_name
+    assert_equal '13 Rue Yves Toudic 75010 Paris, France', result.international_address
+    assert_equal '13 Rue Yves Toudic 75010 Paris, France', result.address
+    assert_equal '13 Rue Yves Toudic 75010 Paris', result.national_address
+    assert_equal '13 Rue Yves Toudic', result.street_address
+    assert_equal '13', result.street_number
+    assert_equal 'Rue Yves Toudic', result.street
+    assert_equal 'Rue Yves Toudic', result.street_name
+    assert_equal 'Paris', result.city
+    assert_equal 'Paris', result.city_name
+    assert_equal '75110', result.city_code
+    assert_equal '75010', result.postal_code
+    assert_equal '75', result.department_code
+    assert_equal 'Paris', result.department_name
+    assert_equal 'ÃŽle-de-France', result.region_name
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(48.870131, result.coordinates[0])
+    assert_equal(2.363473, result.coordinates[1])
+  end
+
+  def test_paris_special_business_logic
+    result = Geocoder.search('paris').first
+    assert_equal 'city', result.result_type
+    assert_equal '75000', result.postal_code
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(2244000, result.population)
+    assert_equal 'Paris', result.city
+    assert_equal 'Paris', result.city_name
+    assert_equal '75056', result.city_code
+    assert_equal '75000', result.postal_code
+    assert_equal '75', result.department_code
+    assert_equal 'Paris', result.department_name
+    assert_equal 'ÃŽle-de-France', result.region_name
+    assert_equal(48.8589, result.coordinates[0])
+    assert_equal(2.3469, result.coordinates[1])
+  end
+
+  def test_city_result_methods
+    result = Geocoder.search('montpellier').first
+    assert_equal 'city', result.result_type
+    assert_equal '34080', result.postal_code
+    assert_equal '34172', result.city_code
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(5, result.administrative_weight)
+    assert_equal(255100, result.population)
+    assert_equal '34', result.department_code
+    assert_equal 'Hérault', result.department_name
+    assert_equal 'Languedoc-Roussillon', result.region_name
+    assert_equal(43.611024, result.coordinates[0])
+    assert_equal(3.875521, result.coordinates[1])
+  end
+
+  def test_results_component_when_reverse_geocoding
+    result = Geocoder.search([48.770431, 2.364463]).first
+    assert_equal '94021_1133_49638b', result.location_id
+    assert_equal 'housenumber', result.result_type
+    assert_equal '4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue, France', result.international_address
+    assert_equal '4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue, France', result.address
+    assert_equal '4 Rue du Lieutenant Alain le Coz 94550 Chevilly-Larue', result.national_address
+    assert_equal '4 Rue du Lieutenant Alain le Coz', result.street_address
+    assert_equal '4', result.street_number
+    assert_equal 'Rue du Lieutenant Alain le Coz', result.street
+    assert_equal 'Rue du Lieutenant Alain le Coz', result.street_name
+    assert_equal 'Chevilly-Larue', result.city
+    assert_equal 'Chevilly-Larue', result.city_name
+    assert_equal '94021', result.city_code
+    assert_equal '94550', result.postal_code
+    assert_equal '94', result.department_code
+    assert_equal 'Val-de-Marne', result.department_name
+    assert_equal 'ÃŽle-de-France', result.region_name
+    assert_equal 'France', result.country
+    assert_equal 'FR', result.country_code
+    assert_equal(48.770639, result.coordinates[0])
+    assert_equal(2.364375, result.coordinates[1])
+  end
+
+  def test_no_reverse_results
+    result = Geocoder.search('no reverse results')
+    assert_equal 0, result.length
+  end
+
+  def test_actual_make_api_request_with_https
+    Geocoder.configure(use_https: true, lookup: :ban_data_gouv_fr)
+
+    require 'webmock/test_unit'
+    WebMock.enable!
+    stub_all = WebMock.stub_request(:any, /.*/).to_return(status: 200)
+
+    g = Geocoder::Lookup::BanDataGouvFr.new
+    g.send(:actual_make_api_request, Geocoder::Query.new('test location'))
+    assert_requested(stub_all)
+
+    WebMock.reset!
+    WebMock.disable!
+  end
+
+
+  private
+
+  def assert_country_code(result)
+    [:state_code, :country_code, :province_code].each do |method|
+      assert_equal 'FR', result.send(method)
+    end
+  end
+end
-- 
GitLab