DEV Community

Cover image for How to Collect Route Waypoints with Address Autocomplete and Map Clicks
Casey Rivers for Geoapify Maps API

Posted on

How to Collect Route Waypoints with Address Autocomplete and Map Clicks

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:


Table of Contents

  1. The Challenge: Multiple Input Methods
  2. Set Up the HTML Structure
  3. Initialize the Map
  4. Add Address Autocomplete
  5. Handle Map Clicks with Reverse Geocoding
  6. Implement Drag and Drop Reordering
  7. Display Numbered Markers
  8. Build the Route
  9. Theme Support
  10. 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

The waypoints array stores coordinates, formatted address, marker reference, and autocomplete instance for each waypoint.

Screenshot showing the initial interface with two empty waypoint inputs and a map


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);
            }
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

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.

Screenshot showing address autocomplete dropdown with suggestions as user types


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);
    }
});
Enter fullscreen mode Exit fullscreen mode

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";
    });
}
Enter fullscreen mode Exit fullscreen mode

The placeholder text changes to indicate which input will receive the next map click.

Screenshot showing a user clicking on the map and the address appearing in the input field


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
Enter fullscreen mode Exit fullscreen mode

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();
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Tip: The scaleFactor=2 parameter provides high-resolution icons for retina displays.

Screenshot showing numbered markers on the map matching the waypoint list colors


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]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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]});
}
Enter fullscreen mode Exit fullscreen mode

Screenshot showing a complete route with multiple waypoints and the route line connecting them


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);
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Set up address autocomplete using the @geoapify/geocoder-autocomplete library with customizable themes
  2. Handle map clicks by reverse geocoding coordinates to addresses
  3. Implement drag and drop for intuitive waypoint reordering
  4. Display numbered markers that update dynamically as waypoints change
  5. 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:


Try It Now

Sign up at geoapify.com to get your API key and start building your own waypoint collection interface.

Top comments (0)