Créer un localisateur de succursales

Vous avez une liste de vos propres emplacements. Un tableur de douze lignes, ou une table de quatre cents. Un client tape son adresse et vous voulez lui montrer le plus proche et comment s'y rendre.

C'est un travail différent de trouver le lieu le plus proche d'un type donné, qui cherche des pharmacies ou des cafés dans le corpus public. Ici le jeu de données est le vôtre, donc le travail se divise en deux : géocodez vos emplacements une fois, puis interrogez votre propre table à chaque visite.

Le code est du TypeScript ordinaire. Pour le câblage React, Next.js, Vue ou Nuxt, partez des recettes par cadriciel.

Géocodez vos emplacements une fois, pas à chaque visite

C'est la partie que l'on prend souvent à l'envers. Géocoder vos douze succursales à chaque chargement de page, c'est douze appels par visiteur pour une réponse qui n'a pas changé depuis l'ouverture du commerce.

Faites-le une fois, gardez les coordonnées, et recommencez quand la liste change. Pour une poignée de lignes, la recherche structurée est le bon outil : vous savez déjà quelle partie est la ville et laquelle est la province, alors dites-le plutôt que de laisser le moteur deviner.

interface Branch {
  id: string;
  name: string;
  address: string;
  city: string;
  region: string;
}
 
const BRANCHES: Branch[] = [
  { id: "yyc-01", name: "Beltline", address: "101 17 Ave SW", city: "Calgary", region: "AB" },
  { id: "yul-01", name: "Centre-ville", address: "1000 rue Sainte-Catherine O", city: "Montréal", region: "QC" },
  { id: "yhz-01", name: "Downtown", address: "5251 Duke St", city: "Halifax", region: "NS" },
];
 
async function locate(branches: Branch[]) {
  const out = [];
  for (const branch of branches) {
    const [hit] = await unmap.geocoder.structured({
      address: branch.address,
      city: branch.city,
      region: branch.region,
      limit: 1,
    });
    out.push({ ...branch, lng: hit?.lng, lat: hit?.lat, quality: hit?.match_type });
  }
  return out;
}
 
const located = await locate(BRANCHES);

Vérifiez match_type avant de faire confiance à une ligne. Une succursale dont l'adresse a résolu en fallback a obtenu la rue plutôt que le bâtiment, et l'épingle se posera à un bout de celle-ci. Ce sont les lignes qui méritent un coup d'oeil humain, et il y en a habituellement deux ou trois dans toute liste réelle. Pour une table plus grande, le géocodage par lots fait la même chose sur un CSV et vous remet un fichier de révision contenant exactement ces lignes.

Trouvez le client

Un appel, et la même idée de focus que partout ailleurs : si vous savez à peu près où il magasine, dites-le. Le classement privilégie les réponses proches sans exclure les lointaines.

const [customer] = await unmap.geocoder.search("Bow Valley Square, Calgary", {
  limit: 1,
  focus: [-114.0719, 51.0447],
});
if (!customer) throw new Error("nous n'avons pas trouvé cette adresse");

Triez votre propre liste par distance

Vos emplacements sont dans votre propre table : c'est donc de l'arithmétique, pas un appel à l'API. La distance orthodromique suffit pour retenir une liste courte.

type Located = { id: string; name: string; lng?: number; lat?: number };
type Pinned = Located & { lng: number; lat: number };
 
function km(a: [number, number], b: [number, number]): number {
  const R = 6371;
  const toRad = (d: number) => (d * Math.PI) / 180;
  const dLat = toRad(b[1] - a[1]);
  const dLng = toRad(b[0] - a[0]);
  const h =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(a[1])) * Math.cos(toRad(b[1])) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(h));
}
 
function shortlistFor(from: [number, number], located: Located[], take = 3) {
  return located
    .filter((b): b is Pinned => b.lng != null && b.lat != null)
    .map((b) => ({ ...b, straightLineKm: km(from, [b.lng, b.lat]) }))
    .sort((a, b) => a.straightLineKm - b.straightLineKm)
    .slice(0, take);
}

Trois est une liste courte raisonnable. L'intérêt de la couper apparaît à l'étape suivante : le temps de trajet coûte un appel de routage chacun, et vous n'en voulez pas un par succursale.

Classez la liste courte par temps de trajet

La distance à vol d'oiseau et le temps de trajet divergent dès qu'une rivière, une gare de triage ou un sens unique s'interpose, et un localisateur qui envoie quelqu'un vers un pont à huit kilomètres en amont se trompe d'une manière que le client remarque.

type Pinned = { id: string; name: string; lng: number; lat: number };
 
async function rankByDriveTime(from: [number, number], shortlist: Pinned[]) {
  const legs = await Promise.all(
    shortlist.map(async (branch) => ({
      branch,
      route: await unmap.router.route(from, [branch.lng, branch.lat], { mode: "auto" }),
    })),
  );
  return legs.sort((a, b) => a.route.durationSeconds - b.route.durationSeconds);
}

durationSeconds et distanceMeters reviennent sur le même résultat : l'étiquette à côté de chaque succursale (« 12 min, 6,4 km ») n'exige aucun second appel.

Tracez le chemin

L'itinéraire que vous avez déjà récupéré porte la géométrie, donc l'afficher ne coûte rien de plus.

import type { RouteResult } from "@unmap/routing";
 
function drawRoute(route: RouteResult) {
  unmap.map!.addSource("leg", {
    type: "geojson",
    data: { type: "Feature", properties: {}, geometry: route.geometry },
  });
  unmap.map!.addLayer({
    id: "leg-line",
    type: "line",
    source: "leg",
    layout: { "line-cap": "round", "line-join": "round" },
    paint: { "line-color": "#FF3E9A", "line-width": 3 },
  });
}

Ce que cela coûte

Géocoder vos succursales est ponctuel : douze emplacements font douze appels, une fois, plus quelques-uns quand vous corrigez les lignes revenues en fallback. Par visiteur, c'est un géocodage pour l'adresse du client plus un appel de routage par succursale retenue : trois succursales font quatre appels par visite. Passer la liste courte de cinq à trois, c'est la différence entre six appels et quatre.

Voir forfaits et limites pour ce qu'est un appel et ce que votre forfait comprend.

Là où cette approche se trompe

  • Des coordonnées périmées. Géocoder une fois suppose de recommencer quand une succursale déménage. Gardez l'adresse d'origine à côté des coordonnées pour savoir d'où vient une ligne.
  • Une succursale jamais géocodée. Le filtre ci-dessus écarte les emplacements sans coordonnées, ce qui est correct et silencieux. Journalisez-les : une succursale absente du localisateur est une succursale que personne ne peut trouver.
  • Le temps de trajet est un modèle, pas une promesse. C'est du routage en circulation libre sur le graphe routier, sans trafic en direct. Traitez-le comme un signal de classement, pas comme une heure d'arrivée.
  • Canada seulement. Un emplacement hors du Canada ne se géocodera pas; voir couverture.

Voir aussi