Autocomplétion d'adresse via l'API BAN
(Base Adresse Nationale)

Site mise à jour le
Cet outil vous permet de gérer l'autocomplétion d'adresse Française dans un champ de formulaire en utilisant l'API de l'État.
Ou
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
  • Alternative à l'API Google Places
  • Simple d'installation
  • Gestion avec script & attributs
  • 100% gratuit
  • Pas de clé API !
  • Solution Made in France 🇫🇷
  • Limite de 50 requêtes API / seconde / IP

Implémentation dans Webflow

Étape #1.

Ajoutez le script en before </body>

<!-- [script by nicolastizio.co] API BAN Adresse -->
<script src="https://cdn.jsdelivr.net/npm/@nicolastizioco/scrypts@latest/src/api-ban/api-ban.min.js"></script>

Où celui-ci pour le customiser

<!-- [script by nicolastizio.co] API BAN Adresse -->
<script>
"use strict";
document.addEventListener("DOMContentLoaded", function () {
  const style = document.createElement("style");
  style.textContent = `
    [data-ban-results]:not([data-ban-results="no-css"]) {
      display: none;
      position: absolute;
      left: 0%;
      top: 100%;
      z-index: 999;
      overflow: auto;
      width: 100%;
      max-height: 15rem;
      padding: 0.5rem;
      border-radius: 0.5rem;
      background-color: white;
      box-shadow: var(--ban-box-shadow, 0 2px 5px 0 rgba(0,0,0,0.2));
    }
    [data-ban-item]:not([data-ban-item="no-css"]) {
      padding: 0.5rem;
      border-radius: 0.2rem;
      transition: all 300ms ease;
      cursor: pointer;
      background-color: transparent;
      scroll-margin: 0.5rem;
    }
    [data-ban-item]:not([data-ban-item="no-css"]).active,
    [data-ban-item]:not([data-ban-item="no-css"]):hover {
      background-color: #eee;
    }
  `;
  document.head.appendChild(style);

  const hasLogWrapper = document.querySelector('[data-ban-wrapper="log"]');

  const logMessage = (shouldLog, ...args) => {
    if (shouldLog) console.log(...args);
  };

  if (hasLogWrapper) {
    console.log("API BAN 🇫🇷 | Made by nicolastizio.co");
  }

  const debounce = (fn, delay) => {
    let timeout;
    return (...args) => {
      clearTimeout(timeout);
      timeout = setTimeout(() => fn(...args), delay);
    };
  };

  const fetchSuggestions = async (query, shouldLog, maxResults = 5) => {
    const apiUrl = `https://data.geopf.fr/geocodage/completion/?text=${encodeURIComponent(
      query
    )}&maximumResponses=${maxResults}&type=StreetAddress`;
    logMessage(shouldLog, "🔎 :", query);
    try {
      const response = await fetch(apiUrl);
      const data = await response.json();
      return data.results || [];
    } catch (e) {
      return [];
    }
  };

  const highlightItems = (items, currentFocus) => {
    items.forEach((item, index) => {
      item.classList.remove("active");
      if (index === currentFocus) {
        item.classList.add("active");
        item.scrollIntoView({ block: "nearest", behavior: "smooth" });
      }
    });
  };

  const handleSelection = (
    { street, zipcode, city, fulltext },
    inputField,
    streetField,
    zipcodeField,
    cityField,
    resultsContainer,
    shouldLog
  ) => {
    if (inputField) {
      inputField.value = fulltext || `${street || ""}, ${zipcode || ""} ${city || ""}`;
      logMessage(shouldLog, "📍 :", inputField.value);
    }
    if (streetField) {
      const addressPart = fulltext ? fulltext.split(",")[0] : street;
      streetField.value = addressPart || "";
      logMessage(shouldLog, "🛣️ :", streetField.value);
    }
    if (zipcodeField) {
      zipcodeField.value = zipcode || "";
      logMessage(shouldLog, "📮 :", zipcodeField.value);
    }
    if (cityField) {
      cityField.value = city || "";
      logMessage(shouldLog, "🏙️ :", cityField.value);
    }
    resultsContainer.style.display = "none";
  };

  const showSuggestions = (
    suggestions,
    query,
    resultsContainer,
    handleClick,
    resetFocus
  ) => {
    const existingItem = resultsContainer.querySelector("[data-ban-item]");
    const originalClass = existingItem ? existingItem.className : "";
    const originalDataBanItem = existingItem ? existingItem.getAttribute("data-ban-item") : "";

    resultsContainer.innerHTML = "";
    resetFocus();
    if (suggestions.length === 0) {
      resultsContainer.style.display = "none";
      return;
    }
    const queryRegex = new RegExp(`(${query})`, "gi");
    suggestions.forEach((result) => {
      const resultItem = document.createElement("div");
      resultItem.setAttribute("data-ban-item", originalDataBanItem);
      if (originalClass) {
        resultItem.className = originalClass;
      }
      const highlightedLabel = result.fulltext.replace(
        queryRegex,
        "<strong>$1</strong>"
      );
      resultItem.innerHTML = highlightedLabel;
      resultItem.addEventListener("click", () => handleClick(result));
      resultsContainer.appendChild(resultItem);
    });
    resultsContainer.style.display = "block";
  };

  document
    .querySelectorAll("[data-ban-results]")
    .forEach((resultsContainer) => {
      const customBoxShadow = resultsContainer.getAttribute("data-ban-results");
      if (customBoxShadow) {
        resultsContainer.style.setProperty("--ban-box-shadow", customBoxShadow);
      }
    });

  document.querySelectorAll("[data-ban-wrapper]").forEach((wrapper) => {
    const shouldLog = wrapper.getAttribute("data-ban-wrapper") === "log";
    wrapper.style.position = "relative";

    const inputField = wrapper.querySelector("[data-ban-input]");
    const resultsContainer = wrapper.querySelector("[data-ban-results]");
    const streetField = wrapper.querySelector("[data-ban-street]");
    const cityField = wrapper.querySelector("[data-ban-city]");
    const zipcodeField = wrapper.querySelector("[data-ban-zipcode]");

    const mainField = inputField || streetField;
    let currentFocus = -1;

    if (!mainField || !resultsContainer) return;

    const maxResultsAttr = resultsContainer.getAttribute(
      "data-ban-results-count"
    );
    const maxResults =
      maxResultsAttr && !isNaN(maxResultsAttr)
        ? Math.min(Math.max(parseInt(maxResultsAttr), 1), 15)
        : 5;
    logMessage(
      shouldLog,
      "Max résultats :",
      maxResults
    );

    const resetFocus = () => {
      currentFocus = -1;
    };

    const performSearch = debounce(async (query) => {
      if (query.length > 2) {
        const suggestions = await fetchSuggestions(
          query,
          shouldLog,
          maxResults
        );
        showSuggestions(
          suggestions,
          query,
          resultsContainer,
          (result) =>
            handleSelection(
              result,
              inputField,
              streetField,
              zipcodeField,
              cityField,
              resultsContainer,
              shouldLog
            ),
          resetFocus
        );
      } else {
        resultsContainer.style.display = "none";
      }
    }, 300);

    mainField.addEventListener("input", () => {
      const query = mainField.value.trim();
      performSearch(query);
    });

    mainField.addEventListener("keydown", (e) => {
      const items = resultsContainer.querySelectorAll("[data-ban-item]");
      if (e.key === "Escape") {
        e.preventDefault();
        resultsContainer.style.display = "none";
        currentFocus = -1;
        return;
      }
      if (!items.length) return;
      if (e.key === "ArrowDown") {
        e.preventDefault();
        currentFocus = (currentFocus + 1) % items.length;
        highlightItems(items, currentFocus);
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        currentFocus = (currentFocus - 1 + items.length) % items.length;
        highlightItems(items, currentFocus);
      } else if (e.key === "Enter") {
        e.preventDefault();
        if (currentFocus > -1 && items[currentFocus]) {
          items[currentFocus].click();
        }
      }
    });

    document.addEventListener("click", (e) => {
      if (!wrapper.contains(e.target)) {
        resultsContainer.style.display = "none";
      }
    });
  });
});
</script>
Étape #2.

Ajouter les attributs dans le formulaire

Le détail en version Desktop  💻
Pour l'adresse complète
Div Form
data-ban-wrapper
=
data-ban-input
=
Adresse
data-ban-results
=
data-ban-item
=
55 Rue du Faubourg Saint-Honoré, 75008 Paris
55 Rue du Faubourg Saint-Vincent, 45000 Orléans
55 Rue du Faubourg, 01120 La Boisse
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
DIV # Définir la zone avec le champ input avec l'autocomplétion
Nom
data-ban-wrapper
Ajouter l'attribut dans une div qui va wrapper l'input de recherche et la div des résultats.
Valeur
log
OPTIONNEL : Pour afficher les logs de requête API dans la console
INPUT # Définir le champ input avec l'autocomplétion
Nom
data-ban-input
Ajouter l'attribut sur l'input qui contiendra le champ de recherche d'adresse.
DIV # Définir la div avec les résultats de recherche
Nom
data-ban-results
Ajouter l'attribut sur la div qui contiendra tous les résultats de recherche. Le script gère la mise en page et le CSS.

OPTIONNEL : Mettre en "display:none" cet élément pour éviter de le voir au chargement de la page.
Valeur
no-css
OPTIONNEL : Pour ne pas considérer le CSS de l'attribut [data-ban-results] présent dans le code.
Si activé, c'est le CSS de la div qui sera utilisé.
DIV - Optionnel # À ajouter au même niveau que data-ban-results
Nom
data-ban-results-count
Permet de définir le nombre d'adresse remonté par l'API.
Valeur
15
Nombre d'items à faire afficher dans la liste.
De 1 à 15 maximum. 5 par défaut.
DIV # Définir la div avec le résultat de recherche unique
Nom
data-ban-item
Ajouter une second div à l'intérieur de [data-ban-results] avec cet attribut afin d'afficher l'item de résultat. Le script gère la mise en page et le CSS.
Valeur
no-css
OPTIONNEL : Pour ne pas considérer le CSS de l'attribut [data-ban-item] présent dans le code.
Si activé, c'est le CSS de la div qui sera utilisé.

IMPORTANT : Une class .active va rajouter en dynamique sur l'élément à la navigation au clavier.
Bien penser à créer un combo class
.ma-custom-class .active.
INPUT • Optionnel # Définir le champ input pour remonter la rue
Nom
data-ban-street
OPTIONNEL : Ajouter un input en display:none pour remonter de manière granulaire l'adresse avec uniquement le numéro + le nom de la rue.
INPUT • Optionnel # Définir le champ input pour remonter la ville
Nom
data-ban-zipcode
OPTIONNEL : Ajouter un input en display:none pour remonter de manière granulaire la ville.
INPUT • Optionnel # Définir le champ input pour remonter le code postal
Nom
data-ban-city
OPTIONNEL : Ajouter un input en display:none pour remonter de manière granulaire le code postal.

Pour l'adresse décomposée

Div Form
data-ban-wrapper
=
data-ban-street
=
Adresse
data-ban-results
=
data-ban-item
=
55 Rue du Faubourg Saint-Honoré, 75008 Paris
55 Rue du Faubourg Saint-Vincent, 45000 Orléans
55 Rue du Faubourg, 01120 La Boisse
data-ban-zipcode
=
Code postal
data-ban-city
=
Ville
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
DIV # Définir la zone avec le champ input plus les résultats avec l'autocomplétion
Nom
data-ban-wrapper
Ajouter l'attribut dans une div qui va wrapper l'input de recherche et la div des résultats.
Valeur
log
OPTIONNEL : Pour afficher les logs de requête API dans la console
INPUT # Définir le champ input avec l'autocomplétion
Nom
data-ban-street
Ajouter l'attribut sur l'input qui contiendra le champ de recherche d'adresse et qui va remonter uniquement la rue de l'adresse.
DIV # Définir la div avec les résultats de recherche
Nom
data-ban-results
Ajouter l'attribut sur la div qui contiendra tous les résultats de recherche. Le script gère la mise en page et le CSS.

OPTIONNEL : Mettre en "display:none" cet élément pour éviter de le voir au chargement de la page.
Valeur
no-css
OPTIONNEL : Pour ne pas considérer le CSS de l'attribut [data-ban-results] présent dans le code.
Si activé, c'est le CSS de la div qui sera utilisé.
DIV • Optionnel # À ajouter au même niveau que data-ban-results
Nom
data-ban-results-count
Permet de définir le nombre d'adresse remonté par l'API.
Valeur
15
Nombre d'items à faire afficher dans la liste.
De 1 à 15 maximum. 5 par défaut.
DIV # Définir la div avec les résultats de recherche unique
Nom
data-ban-item
Ajouter une second div à l'intérieur de [data-ban-results] avec cet attribut afin d'afficher l'item de résultat. Le script gère la mise en page et le CSS.
Valeur
no-css
OPTIONNEL : Pour ne pas considérer le CSS de l'attribut data-ban-item présent dans le code.
Si activé, c'est le CSS de la div qui sera utilisé.

IMPORTANT : Une class .active va rajouter en dynamique sur l'élément à la navigation au clavier.
Bien penser à créer un combo class
.ma-custom-class .active.
INPUT # Définir le champ input pour remonter la ville
Nom
data-ban-zipcode
Ajouter l'attribut sur l'input qui contiendra le champ du code postal.
INPUT # Définir le champ input pour remonter le code postal
Nom
data-ban-city
Ajouter l'attribut sur l'input qui contiendra le champ de la ville.

Foire aux questions

Pourquoi utiliser cette solution plutôt que l'API de Google (Place API) ?

Cette solution maison est bien plus pratique à utiliser que l'API Place de Google.

Chez Google vous êtes obligés de créer un compte développeur, créer une application, ajouter une carte bleue, générer une clé, gérer les droits d'accès et à la fin cela à un vrai coup financier. 👇

De plus, l'intégration de Place API de Google n'est pas facile à intégrer et demande de vraie compétence technique.

Enfin, cela vous éviter d'avoir à envoyer des données à Google sur votre site et votre recherche reste anonyme sur des données et serveurs Français.

Graphique du coup de l'API Place de Google Maps
Est-ce que cette solution est aussi performante que l'API de Google ?

Dans un sens, oui.

L'API fournie par l'État est mise à jour chaque semaine sur la base de nouvelles rues et adresses listées auprès de toutes les villes de France sur la base des "adresses BAL".

Cependant la finesse des premiers résultats dans le champ de recherche n’est pas aussi performance que se propose Google, mais vous trouvez bien votre adresse.

Google piste votre activité et connaît les adresses les plus susceptibles de vous intéresser. Ainsi, il remonte bien plus rapidement une adresse en tapant très peu de caractères, l'API de l'État quant à elle plus neutre.

API BAN c'est quoi ? Quel API est réellement utilisé ?

L'API BAN (Base Adresse Nationale) est un service public français qui permet d'accéder à la base de données officielle des adresses en France. C'est une API gratuite et ouverte qui vous permet de suggérer des adresses pendant que l'utilisateur tape (comme sur Google Maps).

Cette base est collaborative et regroupe les données des communes, d'OpenStreetMap, et d'autres sources officielles et est mise à jour 2 fois par semaine.

L'API est complètement gratuite, sans inscription ni clé d'API nécessaire. Une limite d'usage est appliquée de 50 appels/IP/seconde.

La documentation complète est disponible sur https://adresse.data.gouv.fr/api-doc/adresse et https://geoservices.ign.fr/documentation/services/services-geoplateforme/autocompletion

Update : L'API Adresse BAN est dépréciée et intégrée dans le nouveau Service de géocodage de la Géoplateforme.

Est-ce que des données sont récoltées via votre solution ?

Absolument que Non !

Le script s'exécute sur la machine de l'utilisateur et va uniquement faire un call API et remontez les résultats de votre recherche.

Je ne récupère aucune donnée et l'État non plus.

Comment intégrer cette solution sur mon site Webflow ?

Je vous invite à lire attentivement la doc ci-dessus ☝ et de voir le tutoriel vidéo ici.

Est-ce que je peux customiser le designe des résultats de recherche ?

Oui absolument, le script fonctionne à 99% via des attributs. Seulement 2 attributs contiennent des propriétés CSS en dur que vous pouvez désactiver en ajoutant la valeur "no-css" sur [data-ban-results] & [data-ban-input].

Aussi le code est proposé via un CDN NPM ou à copier sur votre page, donc toute modification est possible.

Est-ce que je peux utiliser cette solution sur Framer, Wordpress, Etc ... ?

Oui absolument, à partir du moment où vous pouvez sur votre CMS ajouter des DIV & attributs alors vous pouvez utiliser cette solution.

Cependant, cette solution a été pensée pour une utilisation sur Webflow. Donc si vous trouvez des incompatibilités sur votre CMS ou des difficultés à l'intégrer, Je vous invite à me contacter pour vous aider.

Est-ce que le script sera maintenu dans la durée ?

Il n'y a pas de raison pour que le script ne marche pas d'ici quelques mois / années.

Tant que l'API n'est pas mise à jour techniquement, le script sera maintenu.

Cette solution est une initiative personnelle qui dans un 1er temps a été créée pour un client spécifique puis partagé pour tous en open-source.