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
- Créer un localisateur du plus proche pour la même forme appliquée au corpus public plutôt qu'à votre liste.
- Géocodage par lots quand la liste est un CSV plutôt qu'un tableau.
- Recherche structurée pour savoir pourquoi les composants valent mieux qu'une seule chaîne.