Ever tried typing "that coffee shop near the park" into a route planner? It does not work. Users know where they want to go, but they do not always know the exact address. Sometimes they want to type "Arc de Triomphe" and pick from suggestions. Other times they just want to click on the map because they recognize the spot visually.
A good waypoint interface supports both. It also needs to handle the inevitable "wait, I want to go there second, not third" moment with easy reordering.
This tutorial walks through building exactly that kind of interface.
What makes a waypoint input actually usable:
- Address autocomplete - catches typos, suggests real places
- Map clicks with reverse geocoding - click a spot, get the address
- Drag and drop reordering - because plans change
- Numbered markers - visual confirmation of what was entered
By the end, you will have a waypoint collection interface that handles how people actually enter locations, not how we wish they would.
Try the live demo:
APIs used:
- Address Autocomplete API - address suggestions as you type
- Reverse Geocoding API - convert map clicks to addresses
- Map Tiles API - base map layer
- Map Marker Icon API - numbered waypoint markers
- Routing API - calculate route between waypoints
Table of Contents
- The Challenge: Multiple Input Methods
- Set Up the HTML Structure
- Initialize the Map
- Add Address Autocomplete
- Handle Map Clicks with Reverse Geocoding
- Implement Drag and Drop Reordering
- Display Numbered Markers
- Build the Route
- Theme Support
- Summary
The Challenge: Multiple Input Methods
Route planning applications need to accept waypoints in different ways. Some users prefer typing addresses and selecting from suggestions. Others want to click directly on the map, especially when they know the location visually but not the exact address. The ideal solution supports both methods seamlessly.
When a user clicks the map, we need to convert those coordinates into a meaningful address. This is called reverse geocoding. The address then appears in the input field, so users can see exactly what location they selected.
Waypoint order matters for routing. Users often need to rearrange stops after adding them. Drag and drop provides an intuitive way to reorder without deleting and re-adding waypoints.
Set Up the HTML Structure
The interface has two main areas: a side panel with waypoint inputs and the map. Load the required libraries:
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link id="geocoder-theme" rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@geoapify/geocoder-autocomplete@3.0.1/styles/minimal.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@geoapify/geocoder-autocomplete@3.0.1/dist/index.min.js"></script>
Create the container for waypoint inputs and the map:
<div class="panel">
<h3>Collect Waypoints</h3>
<p class="hint">Enter addresses or click on the map. Drag rows to reorder.</p>
<div id="waypoints"></div>
<button id="add-btn">Add a new location</button>
<button id="build-route-btn">Build Route</button>
</div>
<div id="map"></div>
The geocoder-autocomplete library provides pre-built CSS themes (minimal, round-borders, and their dark variants).
Key: Sign up at geoapify.com to get your API key.
Initialize the Map
Set up a Leaflet map and initialize the waypoints array:
const API_KEY = "YOUR_API_KEY";
let waypoints = [];
let activeIdx = 0;
const map = L.map("map", {zoomControl: false}).setView([48.8566, 2.3522], 12);
L.control.zoom({position: "bottomright"}).addTo(map);
L.tileLayer(`https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}@2x.png?apiKey=${API_KEY}`, {
attribution: '© Geoapify © OpenMapTiles © OpenStreetMap',
maxZoom: 20
}).addTo(map);
// Initialize with 2 empty waypoints
addWaypoint();
addWaypoint();
The waypoints array stores coordinates, formatted address, marker reference, and autocomplete instance for each waypoint.
Add Address Autocomplete
The @geoapify/geocoder-autocomplete library handles address suggestions. When a user types, it queries the Address Autocomplete API and displays matching addresses.
Create an autocomplete instance for each waypoint input:
function renderList() {
waypoints.forEach((wp, idx) => {
// ... create row HTML with container element id="ac-${idx}" ...
// Setup autocomplete
wp.autocomplete = new autocomplete.GeocoderAutocomplete(
document.getElementById(`ac-${idx}`),
API_KEY,
{placeholder: "Enter address"}
);
wp.autocomplete.on("select", (loc) => {
if (loc) {
setWaypoint(idx, loc.properties.lat, loc.properties.lon, loc.properties.formatted);
}
});
});
}
The select event fires when a user picks a suggestion, providing coordinates and formatted address.
Tip: The library supports multiple themes (minimal, round-borders, and their dark variants). You can switch themes by changing the CSS file loaded in the head.
Handle Map Clicks with Reverse Geocoding
When users click the map, we call the Reverse Geocoding API to convert coordinates to an address. The result fills the first empty waypoint input.
// Map click → reverse geocode → fill first empty waypoint
map.on("click", async (e) => {
if (activeIdx < 0) return;
const {lat, lng} = e.latlng;
const res = await fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${API_KEY}`);
const data = await res.json();
if (data.features?.[0]) {
const p = data.features[0].properties;
setWaypoint(activeIdx, p.lat, p.lon, p.formatted);
}
});
The activeIdx tracks which waypoint should receive the next map click. It points to the first empty waypoint, or -1 if all waypoints have values.
// Find first empty waypoint
function updateActiveIdx() {
activeIdx = waypoints.findIndex(wp => !wp.lat);
waypoints.forEach((wp, idx) => {
const input = document.querySelector(`#ac-${idx} input`);
if (input) input.placeholder = idx === activeIdx ? "Enter address or click map" : "Enter address";
});
}
The placeholder text changes to indicate which input will receive the next map click.
Implement Drag and Drop Reordering
HTML5 drag and drop lets users reorder waypoints. The key is handling the drop event to reorder the array:
row.draggable = true;
row.ondragstart = (e) => {
e.dataTransfer.setData("idx", idx);
};
row.ondrop = (e) => {
e.preventDefault();
const from = +e.dataTransfer.getData("idx");
if (from !== idx) {
waypoints.splice(idx, 0, waypoints.splice(from, 1)[0]);
renderList();
}
};
row.ondragover = (e) => e.preventDefault(); // Allow drop
When dropped, we use splice to move the waypoint in the array, then re-render. This updates marker numbers and colors to match the new order.
Display Numbered Markers
Each waypoint gets a numbered marker using the Map Marker Icon API:
function updateMarker(idx) {
const wp = waypoints[idx];
if (wp.marker) map.removeLayer(wp.marker);
if (!wp.lat) return;
const color = getColor(idx).replace('#', '%23');
const icon = L.icon({
iconUrl: `https://api.geoapify.com/v2/icon?type=awesome&color=${color}&text=${idx + 1}&size=48&contentSize=20&scaleFactor=2&apiKey=${API_KEY}`,
iconSize: [36, 48],
iconAnchor: [18, 48],
popupAnchor: [0, -48]
});
wp.marker = L.marker([wp.lat, wp.lon], {icon, draggable: true}).addTo(map).bindPopup(wp.formatted);
// Drag marker → reverse geocode
wp.marker.on("dragend", async () => {
const {lat, lng} = wp.marker.getLatLng();
const res = await fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${API_KEY}`);
const data = await res.json();
if (data.features?.[0]) {
const p = data.features[0].properties;
wp.lat = p.lat;
wp.lon = p.lon;
wp.formatted = p.formatted;
wp.autocomplete.setValue(p.formatted);
wp.marker.setPopupContent(p.formatted);
clearRoute();
}
});
}
Tip: The
scaleFactor=2parameter provides high-resolution icons for retina displays.
Build the Route
Once waypoints are set, call the Routing API:
async function buildRoute() {
const validWaypoints = waypoints.filter(wp => wp.lat && wp.lon);
if (validWaypoints.length < 2) {
alert("Please add at least 2 waypoints to build a route");
return;
}
const waypointsParam = validWaypoints.map(wp => `${wp.lat},${wp.lon}`).join("|");
const url = `https://api.geoapify.com/v1/routing?waypoints=${waypointsParam}&mode=drive&apiKey=${API_KEY}`;
const res = await fetch(url);
const data = await res.json();
if (data.features?.[0]) {
renderRoute(data.features[0]);
}
}
Render the route as a GeoJSON layer:
function renderRoute(feature) {
clearRoute();
routeLayer = L.geoJSON(feature, {
style: {
color: "#2196F3",
weight: 6,
opacity: 0.8,
lineCap: "round",
lineJoin: "round"
}
}).addTo(map);
map.fitBounds(routeLayer.getBounds(), {padding: [50, 50]});
}
Theme Support
The demo includes light and dark themes. Switch themes by changing the CSS file:
function setTheme(name) {
document.getElementById("geocoder-theme").href =
`https://cdn.jsdelivr.net/npm/@geoapify/geocoder-autocomplete@3.0.1/styles/${name}.css`;
document.body.className = `theme-${name}`;
setMapTiles(name.includes("dark") ? "dark" : "light");
localStorage.setItem("theme", name);
}
The preference is saved to localStorage so users see their chosen theme on return visits.
Summary
Collecting route waypoints requires combining multiple input methods for the best user experience. This tutorial showed how to:
- Set up address autocomplete using the @geoapify/geocoder-autocomplete library with customizable themes
- Handle map clicks by reverse geocoding coordinates to addresses
- Implement drag and drop for intuitive waypoint reordering
- Display numbered markers that update dynamically as waypoints change
- Build routes from collected waypoints using the Routing API
The combination of autocomplete and map clicks covers different user preferences. Some users know exact addresses; others recognize locations visually. Supporting both makes your route planner accessible to everyone.
Key takeaways:
- Address autocomplete reduces errors and speeds up input
- Reverse geocoding converts map clicks to meaningful addresses
- Drag and drop provides intuitive reordering without complex UI
- Numbered markers help users track waypoint order
- Theme support improves accessibility and user preference
Useful links:
- Address Autocomplete API
- @geoapify/geocoder-autocomplete on npm
- Routing API Playground
- Leaflet Documentation
- Map Marker Icon API
Try It Now
Sign up at geoapify.com to get your API key and start building your own waypoint collection interface.





Top comments (0)