Maps

Search, then route

Wire a search box to a map, let the user pick a destination, and draw the route. Includes the coordinate flip that catches everyone.

Search-then-route is the core Maimaps flow: the user types a place, picks a result, and sees the way there. This guide builds it, and along the way covers the single most common integration bug in the whole API.

useSearch gives you execute, plus data, loading, and error. It is safe to call on every keystroke: the hook drops stale responses using a sequence token, so a slow request for "gar" can never overwrite the results for "garki".

import { useSearch } from "@maimaps/react";

function SearchBox({ onPick }) {
  const { execute, data, loading } = useSearch();

  return (
    <>
      <input
        placeholder="Search places…"
        onChange={(e) =>
          execute({
            q: e.target.value,
            latitude: 9.0579,   // bias toward the user
            longitude: 7.4951,
          })
        }
      />
      {loading && <p>Searching…</p>}
      <ul>
        {data?.map((result) => (
          <li key={result.id}>
            <button onClick={() => onPick(result)}>
              {result.name} — {result.distanceKm} km
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Passing latitude and longitude does two things: it ranks nearby results higher, and it populates distanceKm on every hit. Omit them and you get neither.

2. Route to the result

The user has picked a destination. Feed its coordinates to useRoute.

import { useRoute } from "@maimaps/react";

const { execute: getRoute, data: route } = useRoute();

const onPick = (result) =>
  getRoute({
    originLat: 9.0579,
    originLng: 7.4951,
    destLat: result.latitude,
    destLng: result.longitude,
    mode: "drive",
  });

The response is OSRM-shaped. route.routes[0] carries distance in meters, duration in seconds, and geometry as an encoded polyline.

const best = route?.routes[0];
const km = best && (best.distance / 1000).toFixed(1);
const mins = best && Math.round(best.duration / 60);

3. Draw it, and mind the coordinate order

Here is the bug. Read this section even if you skim the rest.

decodePolyline returns [latitude, longitude] pairs, because that is the convention the polyline format uses. But RoutePolyline, RouteShape, and MapLibre generally all want [longitude, latitude], because that is the GeoJSON convention. You have to flip.

import { decodePolyline, RoutePolyline } from "@maimaps/react";

const line = route
  ? decodePolyline(route.routes[0].geometry).map(
      ([lat, lng]) => [lng, lat] as const
    )
  : [];

return (
  <MaimapsMap>
    {line.length > 0 && <RoutePolyline coordinates={line} color="#2563eb" />}
  </MaimapsMap>
);

The rule, stated once: requests take latitude first, response geometry is longitude first. That applies to maneuver.location, waypoints[].location, and every GeoJSON coordinates array in the API.

Flutter sidesteps this entirely. RouteInfo decodes the polyline into List<LatLng> for you:

final route = await Maimaps.instance.routing.getRoute(
  origin: const LatLng(9.0579, 7.4951),
  destination: const LatLng(9.0765, 7.3986),
);

final points = route.polylinePoints;  // ready to draw

4. Add stops

For waypoints, or to route from a loccode instead of a coordinate, drop to the client and call routeAdvanced. There is no dedicated hook for it.

import { useMaimaps } from "@maimaps/react";

const client = useMaimaps();

const trip = await client.routeAdvanced({
  originLoccode: "FC2F 3KN",
  destLat: 9.0765,
  destLng: 7.3986,
  waypoints: [
    { order: 1, latitude: 9.0611, longitude: 7.4702 },
    { order: 2, latitude: 9.0688, longitude: 7.4310 },
  ],
  mode: "drive",
});

Waypoints are visited in ascending order. Give each endpoint either a coordinate pair or a loccode, never neither, or you get a 400 INVALID_PARAMS.

What's next