Un store locator sans Google Maps, c'est possible, et ça tient en trois pièces : un référentiel de points de vente que vous tenez à jour, une recherche « autour de moi » que votre page appelle elle-même avec une clé publiable, et un fond de carte OpenStreetMap servi par OpenFreeMap. Aucun serveur à écrire, aucune clé secrète dans la page, et vos horaires, vos étiquettes et vos adresses restent les vôtres.

Cet article s'adresse à celui qui a la page « Nos magasins » à sa charge, qu'il dirige un réseau ou qu'il code le site. Le début et la fin parlent au premier ; le milieu, avec le code, au second.

Qu'est-ce qu'un store locator ?

Un store locator est la page, ou le module, qui montre à un visiteur les points de vente d'une enseigne autour de lui : il saisit une adresse ou autorise sa position, et la page rend les sites les plus proches avec leur adresse, leurs horaires, leur distance, sur une carte. En français on dit aussi « localisateur de magasins » ou « où trouver nos produits », mais le terme anglais est celui que tout le monde tape.

Derrière cette page il y a toujours les trois mêmes pièces :

  1. Un référentiel de sites : la liste des points de vente avec leur adresse géocodée, leurs horaires, leurs exceptions d'ouverture, leurs étiquettes (drive, terrasse, accès PMR…).
  2. Une recherche de proximité : étant donné un point, quels sites sont à moins de tant de kilomètres, ou de tant de minutes.
  3. Un fond de carte sur lequel poser les résultats.

Google Maps fournit la troisième pièce, et la plateforme Google Places peut fournir la deuxième. Ni l'une ni l'autre ne fournit la première : vos horaires, vos étiquettes et la liste exacte de vos sites ne sont pas dans Google, ils sont chez vous. C'est pour ça que la question « comment faire un store locator sans Google Maps » se pose : ce que Google apporte est remplaçable, ce que vous apportez ne l'est pas.

Pourquoi s'en passer

Le cas le plus fréquent est la page « Nos magasins » en liste figée : vingt adresses dans une page HTML, mises à jour quand quelqu'un y pense, et un site qui a déménagé reste trois mois à l'ancienne adresse. Vient ensuite le plugin branché sur Google Maps. Il marche, jusqu'au jour où la clé Google lisible dans la page est réutilisée ailleurs, ou jusqu'à la facture : les API Google Maps sont facturées à l'appel au-delà d'un crédit mensuel, et un store locator appelle la carte, le géocodage et parfois Places à chaque visite. Et il y a l'agence qui recode la même chose pour chaque client, import d'adresses, géocodage maison, carte, tout à refaire au site suivant.

Dans les trois cas, le problème n'est pas la carte. C'est le référentiel : personne ne le tient, ou chacun le tient à sa façon.

Les trois pièces, sans Google

Pièce Avec Google Sans Google
Fond de carte Google Maps JavaScript API, facturée à l'affichage Tuiles vectorielles OpenFreeMap (données OpenStreetMap), gratuites, à créditer
Recherche « autour de moi » Places API, qui cherche dans la base mondiale de Google GET /locations/search de TrustyData, qui cherche dans votre référentiel
Référentiel de sites Fiches Google Business, éditées une par une Mes lieux : import de fichier, géocodage BAN, horaires, étiquettes, synchronisation
Clé dans la page Clé Google restreinte par domaine Clé publiable tdp_…, un seul endpoint, domaines déclarés, projection réduite

La différence de fond est à la deuxième ligne. Google Places cherche dans tous les lieux du monde, ce qui est utile pour trouver un restaurant, inutile pour afficher vos magasins : il faut filtrer, et vous ne contrôlez ni les fiches ni les horaires qu'il rend. Une recherche dans votre propre référentiel ne connaît que vos sites, et rend exactement ce que vous y avez mis.

La clé publiable, dite en clair

Une clé publiable est une clé d'API faite pour être lue dans le code JavaScript d'une page publique. Elle se reconnaît à son préfixe tdp_, elle n'ouvre qu'un seul endpoint, GET /locations/search, et elle ne fonctionne que depuis les domaines déclarés sur elle. Votre page appelle l'API directement, sans que votre serveur relaie l'appel et sans que votre clé secrète quitte vos variables d'environnement.

Ce qu'elle rend est une projection réduite de chaque site : le nom public, l'adresse en lignes postales, les coordonnées, les horaires et le statut d'ouverture calculé, les étiquettes, les contacts et liens que vous avez choisi de publier, la distance. Ce qu'elle ne rend jamais : le nom interne, le statut du site, l'état du géocodage, les champs techniques. Une recherche ne rend que des sites actifs.

Deux vérités sont à lire avant de publier, parce qu'elles changent ce que vous mettez dans le compte qui émet la clé.

La première : la restriction par domaine n'est pas une frontière de sécurité. Elle empêche la réutilisation opportuniste d'une clé trouvée dans votre code et collée dans une autre page. Elle n'arrête pas un appel forgé hors navigateur, un curl fabrique un en-tête Origin en une ligne, et rien ne peut l'en empêcher.

La seconde : le compte qui émet la clé est public, au sens plein. La clé porte l'identifiant du compte, et qui la lit peut interroger n'importe quel référentiel de ce compte en devinant son code. Ne mettez sur ce compte que ce que vous accepteriez de voir affiché n'importe où. Un site pas encore ouvert, un commentaire interne, un contact direct de responsable vivent sur un autre compte, servi depuis votre serveur avec sa clé secrète.

Le reste est de l'intendance : les domaines se déclarent en https seulement, jusqu'à dix par clé, avec un joker possible sur les sous-domaines ; pas de localhost, parce que n'importe qui peut servir une page en local. La création demande un code TOTP. Le plan Starter donne droit à 2 clés actives, Growth à 5, Business à 10 ; Discovery n'en émet pas. Plusieurs clés actives, c'est ce qui permet une rotation sans coupure. Le détail est dans le guide de la clé publiable.

Le code, une fois pour toutes

Voici une page complète : une carte MapLibre GL sur le style Positron d'OpenFreeMap, un champ d'adresse, et les sites du référentiel boutiques dans un rayon de 10 km, posés sur la carte avec leur distance. Il n'y a rien d'autre à installer.

<link href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" rel="stylesheet">
<script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>

<form id="recherche">
  <input name="adresse" placeholder="Votre adresse ou votre ville" required>
  <button>Trouver le magasin le plus proche</button>
</form>
<div id="carte" style="height: 480px"></div>
<ol id="liste"></ol>
<p>Fond de carte © <a href="https://openfreemap.org">OpenFreeMap</a> ·
   © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, ODbL</p>

<script>
const CLE = "tdp_live_VOTRE_CLE_PUBLIABLE";
const API = "https://api.trustydata.app/services/v1/locations/search";

const carte = new maplibregl.Map({
  container: "carte",
  style: "https://tiles.openfreemap.org/styles/positron",
  center: [2.35, 46.6], zoom: 5,
});
let marqueurs = [];

async function chercher(adresse) {
  const url = API + "?referentiel=boutiques&rayon=10&limit=10"
    + "&adresse=" + encodeURIComponent(adresse);
  const reponse = await fetch(url, { headers: { Authorization: "Bearer " + CLE } });
  if (!reponse.ok) {
    const erreur = await reponse.json().catch(() => null);
    // branchez vos messages sur `detail.code`, jamais sur la phrase
    console.warn("TrustyData", reponse.status, erreur?.detail?.code);
    return;
  }
  const data = await reponse.json();
  marqueurs.forEach((m) => m.remove());
  marqueurs = [];
  const liste = document.getElementById("liste");
  liste.innerHTML = "";
  for (const site of data.resultats) {
    const li = document.createElement("li");
    const km = Math.round(site.distance_m / 100) / 10;
    li.textContent = `${site.nom_public}, ${site.ligne_voie}, ${site.code_postal} `
      + `${site.commune} · ${km} km · ${site.ouverture.statut}`;
    liste.appendChild(li);
    const marqueur = new maplibregl.Marker().setLngLat([site.longitude, site.latitude]);
    marqueur.setPopup(new maplibregl.Popup().setText(site.nom_public)).addTo(carte);
    marqueurs.push(marqueur);
  }
  if (data.resultats.length) {
    const c = data.point_central;
    carte.flyTo({ center: [c.lon, c.lat], zoom: 11 });
  }
}

document.getElementById("recherche").addEventListener("submit", (e) => {
  e.preventDefault();
  chercher(new FormData(e.target).get("adresse"));
});
</script>

Quelques points à connaître sur cet appel :

  • adresse accepte une adresse complète, un nom de commune ou un code postal. La réponse dit laquelle des trois formes elle a résolue dans point_central.precision : un rayon de 10 km autour d'un numéro de rue et autour du centre de Paris ne veulent pas dire la même chose.
  • rayon va de 1 à 50 km, à vol d'oiseau, et chaque site porte sa distance_m. À partir du plan Growth, duree (1 à 30 minutes) et mode (auto, pieton, velo) remplacent le rayon par un temps de trajet réel, et distances=true ajoute la distance routière aux 25 premiers résultats. La réponse porte alors un champ attribution à afficher sur la page : le calcul vient d'OpenStreetMap.
  • tags filtre sur les étiquettes que vous avez posées sur vos sites (plan Growth) : tags=drive ne rend que les sites qui ont un drive.
  • Un refus revient avec un code stable dans detail.code, lisible par votre JavaScript. Branchez vos messages dessus, jamais sur la phrase française qui l'accompagne.

Le navigateur envoie l'en-tête Origin tout seul, et vous ne pouvez pas le remplacer ; la requête de pré-vol déclenchée par Authorization est traitée par l'API.

Sur WordPress

Il n'existe pas d'extension TrustyData pour WordPress. Le plus simple est un bloc HTML personnalisé dans la page « Nos magasins », qui contient le code ci-dessus tel quel ; si votre thème charge déjà MapLibre, retirez les deux premières lignes. Un shortcode maison fait la même chose pour les réutiliser sur plusieurs pages.

Sur Shopify

Une section Liquid dans votre thème, avec le même HTML et le même script. Déclarez le domaine de votre boutique sur la clé (https://votre-boutique.myshopify.com et votre domaine propre, les deux), le joker ne couvrant pas le domaine nu.

Sur un site maison

Le code ci-dessus, dans n'importe quelle page. Pour mettre au point sans localhost, déclarez le domaine de votre environnement de recette sur la clé.

Mettre les sites dedans

La recherche ne vaut que par ce qu'il y a dans le référentiel. Il se remplit de trois façons, selon la taille du réseau.

Avec un fichier, depuis l'espace client Mes lieux : un CSV ou un classeur Excel, vérifié ligne à ligne avant l'import, les valeurs par défaut (horaires, liens, étiquettes) saisies dans l'écran plutôt que dans le fichier. Un identifiant déjà présent est écarté, jamais écrasé.

À la main, site par site, pour un réseau de quelques adresses : nom public, adresse, horaires de la semaine, exceptions comme la fermeture du 15 août, étiquettes.

Par l'API, pour un réseau tenu dans un ERP : PUT remplace un site, PATCH le complète, et sites:sync aligne le référentiel sur votre export en désactivant ce qui a disparu, sans jamais rien supprimer. Si l'export est tronqué et supprimerait plus de la moitié des sites actifs, la synchronisation refuse, sauf confirmation explicite.

Chaque adresse est géocodée en tâche de fond contre la Base Adresse Nationale, le référentiel officiel de l'IGN. Une adresse qui ne se résout qu'à la commune n'est pas placée au centre-ville par défaut : elle reste « en attente », et vous la corrigez en déplaçant le marqueur sur la carte de l'espace client. Un site n'apparaît jamais là où il n'est pas.

La capacité est incluse dans le plan : 200 sites en Starter, 2 000 en Growth, 10 000 en Business, sans facturation au site.

Ce que ça ne fait pas

Pas d'avis, pas de photos, pas de fiche Google Business : un store locator montre vos sites sur votre site. La présence de vos sites sur Google, les avis et leurs réponses relèvent d'outils de présence en ligne comme Partoo ou Uberall, et les deux se cumulent.

Pas de base mondiale de lieux : Google Places et Woosmap savent où est le restaurant le plus proche, n'importe lequel. Ici, la recherche ne connaît que les sites que vous avez mis dans le référentiel.

Pas de calcul sans attribution : dès qu'une recherche demande un temps de trajet ou des distances routières, la page doit porter la mention OpenStreetMap que la réponse lui donne, et le fond de carte OpenFreeMap se crédite dans tous les cas.

Voir le résultat sans rien installer

La démo fait exactement ce que décrit cet article sur un référentiel fictif de 1 000 « Trusty Burger » : une adresse, un rayon ou un temps de trajet, les sites les plus proches sur la carte, et le code de l'appel en cURL, Python ou JSON.

Tester le store locator dans la démo, sans compte.

Pour aller plus loin : le guide Mes lieux décrit le référentiel et ses écritures, le guide de la clé publiable les domaines et la projection réduite, et la page Point de vente le plus proche les plans.