OSRM Table API: Free and open source distance matrix API

In this final installment of our series on Open Source Routing Machine (OSRM), we look at the OSRM Table API, a fast and performant distance matrix API that calculates the distances and travel times between a set of points. We'll then use it to develop a restaurant search application that guides you to the closest restaurant with a desired ⭐️ star rating and 💲 price point (source code: frontend  +  backend).

Restaurant search application powered by the OSRM Table API

Part 1: Introduction to OSRM: Setting up osrm-backend using Docker
Part 2: Hosting the OSRM API on EC2: Running osrm-backend as a web service
Part 3: OSRM Route API: Free turn by turn directions and polylines
Part 4: OSRM Table API: Free distance matrix API (this article)

Hosting OSRM on your server offers a significant advantage when using the Table API - it serves as a cost-free alternative to the Google Maps Distance Matrix API and, importantly, it does not impose a limit on the number of elements per request. This lack of restriction on elements - unlike Google's Distance Matrix API, which has a strict cap at 625 elements (for example, 25 origins multiplied by 25 destinations), makes the OSRM Table API particularly suitable for data-heavy tasks such as route optimization.

What is a distance matrix API?

A distance matrix API is a web service that returns the distances (or travel times) between two or more points. This is almost always in the form of a table i.e. a 2D array D where the i-th - j-th entry D[i][j] refers to the distance between locations i and j. For example, let's say we are at location 0 (distance matrices are almost always zero-indexed) and want to generate a distance matrix that includes locations 1 and 2 that are 2 km and 6 km away respectively. Here's a graphical representation of the problem, with nodes representing the physical locations and the edges representing the distances between them.

Distance matrix represented as a graph

The corresponding distance matrix D looks like this:

0 1 2
0 0 → 0 (0 km) 0 → 1 (6 km) 0 → 2 (2 km)
1 1 → 0 (6 km) 1 → 1 (0 km) 1 → 2 (8 km)
2 2 → 0 (2 km) 2 → 1 (8 km) 2 → 2 (0 km)

To get the distance between locations 1 and 2, all we need to do is look up entry D[1][2] to get the answer (8 km).

🌎
Many companies provide commercial distance matrix API services. Two of them that we've reviewed on this blog are Google Maps (Google LMFS: ComputeRouteMatrix API) and Mapbox (Real-time traffic with the Mapbox Matrix API).

OSRM Table API example

For this example, we'll use the OSRM server loaded with the map of British Columbia, Canada that we set up in Hosting the OSRM API on EC2: Running osrm-backend as a web service to calculate the distances between an address A* (0) in Vancouver's tony West Point Grey neighborhood:

0: 4532 West 4th Avenue, Vancouver, BC (49.268,-123.212)

to two popular restaurants (B and C):

1: Togo Sushi UBC, 3380 Shrum Lane, Vancouver, BC (49.253,-123.237)
2: Raisu, 2340 W 4th Ave, Vancouver, BC (49.268,-123.160)

OSRM Table API call with one origin and two destinations

Once osrm-backend is up and running, take note of its URL, {osrm_server_url}. Alternatively, you can you can use the publicly available test server https://routing.openstreetmap.de/routed-car but this comes with strict usage limits.

Endpoint GET 
http://{osrm_server_url}:5000/table/v1/driving/
-123.2100192,49.2688067;-123.2353903,49.2539939;-123.160899,49.2680595?
annotations=distance

The general structure of a Route API call is as follows:

/{service}/{version}/{profile}/{coordinates}?option=value&option=value

:5000 is the default port OSRM comes installed with.

/table/v1/driving/ instructs OSRM to call the Table Service API (ver 1) with the driving profile.

-123.2100192,49.2688067;-123.2353903,49.2539939;-123.160899,49.2680595 are the GPS coordinates (in longitude, latitude format) of the locations you want to generate a distance matrix for.

annotations=distance specifies that you want the distance matrix to return distances (in meters) only. If you want both durations (travel times) and distances, use annotations=distance,duration.

Output

{
    "code": "Ok",
    "distances": [
        [
            0,
            3627.8,
            3648.2
        ],
        [
            3688.3,
            0,
            7083.6
        ],
        [
            3653.7,
            7029.9,
            0
        ]
    ],
    "destinations": [
        {
            "hint": "OHCjg5wlAoZTAQAAiAIAACcAAAAlAAAAhoGNQoXPBkPycgJBu5r0QKkAAABEAQAAFAAAABIAAACXYAAA1Pan-GTH7wLd9qf4R8jvAgEA_xDOM3Il",
            "distance": 25.254312466,
            "name": "",
            "location": [
                -123.210028,
                49.26858
            ]
        },
        {
            "hint": "YAOLgz2KoYM5AAAAFAAAAAwAAAA_AAAAYx8fQmJ0XEEe4wdBOHYuQjkAAAAUAAAADAAAAD8AAACXYAAA4ZKn-BuO7wLCk6f4ao7vAgIAvxTOM3Il",
            "distance": 18.581253201,
            "name": "Shrum Lane",
            "location": [
                -123.235615,
                49.253915
            ]
        },
        {
            "hint": "xNsAgB09Go1QAAAAOAAAAB0AAAAeAAAAk6WzQu8yd0JtZR5B6oI0QVAAAAA4AAAAHQAAAB4AAACXYAAAxbao-CrG7wK9tqj4XMXvAgMAvwXOM3Il",
            "distance": 22.917697769,
            "name": "West 4th Avenue",
            "location": [
                -123.160891,
                49.268266
            ]
        }
    ],
    "sources": [
        {
            "hint": "OHCjg5wlAoZTAQAAiAIAACcAAAAlAAAAhoGNQoXPBkPycgJBu5r0QKkAAABEAQAAFAAAABIAAACXYAAA1Pan-GTH7wLd9qf4R8jvAgEA_xDOM3Il",
            "distance": 25.254312466,
            "name": "",
            "location": [
                -123.210028,
                49.26858
            ]
        },
        {
            "hint": "YAOLgz2KoYM5AAAAFAAAAAwAAAA_AAAAYx8fQmJ0XEEe4wdBOHYuQjkAAAAUAAAADAAAAD8AAACXYAAA4ZKn-BuO7wLCk6f4ao7vAgIAvxTOM3Il",
            "distance": 18.581253201,
            "name": "Shrum Lane",
            "location": [
                -123.235615,
                49.253915
            ]
        },
        {
            "hint": "xNsAgB09Go1QAAAAOAAAAB0AAAAeAAAAk6WzQu8yd0JtZR5B6oI0QVAAAAA4AAAAHQAAAB4AAACXYAAAxbao-CrG7wK9tqj4XMXvAgMAvwXOM3Il",
            "distance": 22.917697769,
            "name": "West 4th Avenue",
            "location": [
                -123.160891,
                49.268266
            ]
        }
    ]
}

distances is a 2D array representing a matrix in row-major order, meaning that distances[i][j] indicates the travel distance from location i to location j. In the provided example, the following coordinates are sequentially mapped to the sources array:

0: -123.2100192,49.2688067

1: -123.2353903,49.2539939

2: -123.160899,49.2680595

Therefore, location 0, which is 4532 West 4th Avenue, corresponds to the first coordinate (-123.2100192,49.2688067), and the subsequent locations follow in the same order. To find the distance from location 0 (4532 West 4th Avenue, Vancouver, BC) to location 1 (Togo Sushi UBC, 3380 Shrum Lane, Vancouver, BC), you would use distances[0][1]. In this case, it shows a distance of 3627.8 meters, which is very close to the 3448 meters returned by Google Maps.

Driving directions from the Google Maps API closely match that from OSRM

This small difference can be attributed to the different data sources used by Google Maps and OSRM. OSRM uses Open Street Maps (OSM) which relies heavily on volunteer contributions and open data sources while Google Maps uses a combination of satellite imagery, Street View, and data from local governments and organizations.

Restaurant search app

To explore a practical application of the OSRM Table API, we'll build a web based restaurant search application that lets you find nearby restaurants that match your desired ⭐️ star rating and 💲 price point. The Google Maps Nearby Search API is used to gather a list of restaurants, which are ranked based on their distance from your specified location. This ranking is done using the distance matrix of the Table API, which we discussed earlier (source code: frontend  +  backend).

Here's what we'll build - a restaurant search application that ranks nearby restaurants

To get started, install and run both osrm_table_api_frontend and osrm_table_api_backend locally. We'll then work our way through the code and see how a call to the Google Maps Nearby Search API is used to populate restaurant locations on the map. Additionally, we will see how, when paired with the OSRM Table API, this setup allows us to sort restaurants by distance, starting from the nearest and progressing to the farthest.

osrm_table_api_frontend

osrm_table_api_frontend is a simple React app that displays the map layer, marker overlays, location search bar and restaurant listing table. After you've cloned the repository, the first order of business is to set up the .env environment variables. In terminal (Windows users can use PowerShell), open up the osrm_table_api_frontend folder and in the app's root directory, use a text editor like nano to create a .env file by running nano .env. Type in the following and save the file (ctrl + s).

REACT_APP_MAPBOX_TOKEN={MAPBOX_ACCESS_TOKEN}
REACT_APP_API_URL='http://localhost:3001'
REACT_APP_GOOGLE_API_KEY={GOOGLE_API_KEY}

REACT_APP_API_URL indicates that the paired "API" backend, osrm_table_api_backend, runs on port 3001.

REACT_APP_GOOGLE_KEY and REACT_APP_MAPBOX_ACCESS_TOKEN are your Google Maps (retrieving your Google API key) and Mapbox (how do I set up my Mapbox access token?) API keys respectively.

Run npm install to install the required dependencies and npm start to spin up the server. Fire up localhost:3000 in your browser and you should see an empty map centered on Vancouver, Canada. The location address bar should work just fine but pressing the [ 🔍 ] button will return a 400 bad request response.

osrm_table_api_backend

To get the app working, clone osrm_table_api_backend in a new tab and set up the .env file in the root directory. Replace {GOOGLE_API_KEY} with the same key you used in osrm_table_api_frontend.

NODE_ENV=''
PORT='3001'
GOOGLE_API_KEY={GOOGLE_API_KEY}

Save the file and again, run npm install followed by npm start. With both apps up and running, pressing the blue search button [ 🔍 ] button should return a list of nearby restaurants to your location.

Nearby Search API example

The Google Maps Nearby Search API allows users to search for nearby establishments and points of interest, such as bars, restaurants, gas stations, and ATMs, mirroring the local search capability found on Google Maps (note: in this blog post we'll be using the older GET version of Nearby Search - it's easier to use than the newer POST API).

The local search feature on Google Maps lets you find nearby places with a single click

Endpoint GET

https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=49.2688067,-123.2125941
&radius=2500
&type=restaurant
&key=GOOGLE_API_KEY

location refers to the geographic coordinates (in latitude, longitude format) of your current location.

radius is the search radius in meters, so radius=2500 therefore returns all valid points of interest within a 2500 m radius of the specified location.

type restricts the results to places matching the specified type. Only one type may be specified. Supported types include "restaurant", "plumber" and "gym", among many others.

key is your Google Maps API key. Watch this video for a step by step walkthrough on how to generate an API key for your project.

Output

{
    "html_attributions": [],
    "results": [
        {
            "business_status": "OPERATIONAL",
            "geometry": {
                "location": {
                    "lat": 49.2687435,
                    "lng": -123.1854349
                },
                "viewport": {
                    "northeast": {
                        "lat": 49.2700363302915,
                        "lng": -123.1840715697085
                    },
                    "southwest": {
                        "lat": 49.2673383697085,
                        "lng": -123.1867695302915
                    }
                }
            },
            "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/restaurant-71.png",
            "icon_background_color": "#FF9E67",
            "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/restaurant_pinlet",
            "name": "La Quercia",
            "opening_hours": {
                "open_now": false
            },
            "photos": [
                {
                    "height": 3072,
                    "html_attributions": [
                        "\\u003ca href=\"https://maps.google.com/maps/contrib/112924311757606934120\"\\u003eShaun Liu\\u003c/a\\u003e"
                    ],
                    "photo_reference": "AWU5eFgxYlZ5DNwbGVQbPx1JWIwk6fwZRr0akp6ItGVGUYB8ufNuLTXtJDYesN9H2YlGMPFluum3N-r7PmRRC-n6rWn4XORGLMJHDdf4IpB0N-BJn7F_6M_erpqOSBIUWHx3RY6PFhSFg60U2zuPbEnHQEx8Cs2CKYbuxbbTE8ImP7Ra0yNS",
                    "width": 4080
                }
            ],
            "place_id": "ChIJ34VbDlZyhlQRQ9iaa47Y7rI",
            "plus_code": {
                "compound_code": "7R97+FR Vancouver, BC, Canada",
                "global_code": "84XR7R97+FR"
            },
            "price_level": 3,
            "rating": 4.6,
            "reference": "ChIJ34VbDlZyhlQRQ9iaa47Y7rI",
            "scope": "GOOGLE",
            "types": [
                "restaurant",
                "food",
                "point_of_interest",
                "establishment"
            ],
            "user_ratings_total": 384,
            "vicinity": "3689 West 4th Avenue, Vancouver"
        }
    ],
    "status": "OK"
}

In the response returned by the Nearby Search API, the results array is simply a list of place objects that describe the place in more detail. Because we've deliberately restricted our search to minprice=3, only one restaurant, La Quercia (fantastic homestyle Italian food - you should try it if you ever visit Vancouver), is returned in the search results.

rating Contains the place's rating, from 1.0 to 5.0, based on aggregated user reviews.

user_ratings_total is the total number of reviews, with or without text, for this place.

price_level is how expensive the restaurant is relative to others in the same general location. This value ranges from 0 (free) to 4 (very expensive).

name contains the human-readable name for the returned result, which for many businesses is simply the business name.

vicinity is the simplified address for the place, including the street name, street number, and locality, but not the province/state, postal code, or country.

💡
To view a comprehensive list of the fields returned by the Nearby Search API, consult the official documentation on Nearby Search Place Attributes.

Displaying Nearby Search API results

We'll use the output to display each place in the results array as a list item, ordered by distance from the user's current location like so:

When the user clicks on the blue search button [ 🔍 ], the frontend App.js in osrm_table_api_frontend:

/* App.js (osrm_table_api_frontend) */
<button
  onClick={handleSearchPlaces}
  disabled={loading || !yourLocation.latitude}
>
  <img width={24} alt="Search places icon" src={SearchIcon} />
</button>;

calls the handleSearchPlaces() method,

/* App.js (osrm_table_api_frontend) */
import searchPlaces from "./api/searchPlaces";
import { RATING_OPTIONS, RESTAURANT_PLACE_TYPE } from "./constants";

const handleSearchPlaces = async () => {
  try {
    setLoading(true);
    const placesRes = await searchPlaces(RESTAURANT_PLACE_TYPE, yourLocation);
    const placesWithDistance = await fulfillDistanceToPlaces(placesRes);
    setOriginalRestaurants(placesWithDistance);
    setAndSortRestaurants(placesWithDistance);
  } finally {
    setLoading(false);
  }
};

which in turn calls the searchPlaces() method imported from /api/searchPlaces.js.

/* /api/searchPlaces.js (osrm_table_api_frontend) */
import { DEFAULT_SEARCH_PLACE_RADIUS } from "../constants";

export default async function searchPlaces(placeType, coord, radius) {
  try {
    // Constructing the API URL for searching places
    const apiUrl = `${process.env.REACT_APP_API_URL}/search-places?latitude=${
      coord.latitude
    }&longitude=${coord.longitude}&radius=${
      radius || DEFAULT_SEARCH_PLACE_RADIUS
    }&placeType=${placeType}`;

    // Making a fetch request to the API
    const response = await fetch(apiUrl);

    // Parsing and returning the JSON response
    return response.json();
  } catch (error) {
    // Logging an error message if there's an issue with the API request
    console.error("There was an error while searching places:", error);
  }
}

The function searchPlaces() requires three arguments: placeType, coord, and radius. The placeType parameter is a string with the value "restaurant", which is imported from /constants/index.js. The coord parameter is a tuple containing {latitude, longitude}, derived from the address provided in the autocomplete text box. Lastly, radius specifies the search radius in meters and is also imported from /constants/index.js."

The last step is to call our backend (osrm_table_api_backend) at the /searchPlaces endpoint:

/* index.js (osrm_table_api_backend) */

app.get("/search-places", async (req, res) => {
  const { latitude, longitude, radius, placeType } = req.query;

  if (!latitude || !longitude || !placeType) {
    return res.status(400).send("Bad request");
  }

  try {
    const placesRes = await axios.get(
      `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${latitude},${longitude}&radius=${radius}&type=${placeType}&key=${process.env.GOOGLE_API_KEY}`,
    );
    res.status(200).json(placesRes.data?.results);
  } catch (err) {
    res.status(500).send("System error!");
  }
});

The code in index.js uses the well known async/await design pattern to call the https://maps.googleapis.com/maps/api/place/nearbysearch endpoint, and by making the actual request to the Nearby Search API in the backend, we get to keep our GOOGLE_API_KEY private.

Mapping the output of the Nearby Search API to our restaurant listing on the left hand side is straightforward: restaurant name, rating, price_level and vicinity can easily be found in the place object returned.

The only complication comes with displaying the restaurant photo for each listing. The Nearby Search API returns a photo_reference string in the photos array of the place object, which looks something like "AWU5eFhNiFGhXgzY89b91eLP6RCfs-_cobnSXN7DqKlyV6mkQKS".

The PlacePhoto API lets you retrieve a specific photo by making a single GET API call using the photo_reference string like this:

https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photo_reference=photo_reference&key=YOUR_API_KEY

Unfortunately, this means exposing your Google Maps API key on the frontend which is a big security risk. The way to deal with this is to have your frontend call the backend (here, we use the /google-image endpoint):

/* App.js (osrm_table_api_frontend) */
<img
  className="left-image"
  alt="Address control icon"
  src={
    photos?.length > 0
      ? `${process.env.REACT_APP_API_URL}/google-image?photo_reference=${photos[0].photo_reference}&maxwidth=400`
      : RestaurantImg
  }
/>;

And in the backend, make a passthrough call to the PlacePhoto API and return the canonical image URL:

/* index.js (osrm_table_api_backend) */

app.get("/google-image", async (req, res) => {
  try {
    const { photo_reference, maxwidth } = req.query;

    if (!photo_reference || !maxwidth) {
      return res.status(400).json({ error: "Missing required parameters" });
    }

    const imageUrl = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=${maxwidth}&photo_reference=${photo_reference}&key=${GOOGLE_API_KEY}`;
    const response = await axios.get(imageUrl, { responseType: "arraybuffer" });

    if (response.headers["content-type"]) {
      res.header("Content-Type", response.headers["content-type"]);
    }

    res.send(response.data);
  } catch (error) {
    console.error("Error fetching image:", error.message);
    res.status(500).json({ error: "Internal server error" });
  }
});

Ordering the restaurant listing by distance

As a final step, let’s go ahead and use the Table API to sort the restaurants in our list by distance to our current location.

First, we create a list of coordinates by concatenating the the longitude, latitude pair of our current location with the longitude, latitude pairs of the restaurants returned by the Nearby Search API. In the fulfillDistanceToPlaces() method of App.js:

/* App.js fulfillDistanceToPlaces() (osrm_table_api_frontend) */

const coordinates = `${yourLocation.longitude},${
  yourLocation.latitude
};${restaurantList
  ?.map(
    (item) =>
      `${item?.geometry?.location?.lng},${item?.geometry?.location?.lat}`,
  )
  ?.join(";")}`;

With the coordinates array (here's an example of what it looks like) now in place,

we can use the getDistances() method, which in turn directly accesses the OSRM Table API, as shown below:

/* /api/getDistances.js (osrm_table_api_frontend) */

export default async function getDistances(coordinates) {
  try {
    const response = await fetch(
      `https://router.project-osrm.org/table/v1/car/${coordinates}?sources=0&destinations=all&annotations=distance`,
    );

    return response.json();
  } catch (error) {
    console.error("There was an error while fetching directions:", error);
  }
}

Because we've set sources=0&destinations=all, OSRM returns distances, a 1 x (n + 1) matrix (i.e. an array) that lists the distance from the origin location to the origin location (which by definition is 0), from the origin to the first restaurant, from the origin to the second restaurant and so on.

We use this structure to inject the distance value (offsetting distances by 1 to account for the origin location which should be left out) into each restaurant in our list.

/* App.js fulfillDistanceToPlaces() (osrm_table_api_frontend) */

return restaurantList.map((item, idx) => ({
  ...item,
  distance: distances[idx + 1],
}));

And finally, we sort the list of restaurants by distance to the origin and retrieve driving directions from our current (origin) location to the nearest one.

/* App.js getDirectionToNearestRestaurant() (osrm_table_api_frontend) */

const sortedRestaurants = await _orderBy(restaurantList || [], "distance");
const res = await getDirection(yourLocation, {
  latitude: sortedRestaurants[0]?.geometry?.location?.lat,
  longitude: sortedRestaurants[0]?.geometry?.location?.lng,
});

All of this is handled in the useEffect() hook which runs each time the user interacts with the ⭐️ star rating and 💲 price point drop down menus, or hits the the blue search button [ 🔍 ] button.

The final result - driving directions to the nearest restaurant!

If everything works, the app will display a blue polyline from your current location to the nearest restaurant.

Summary and conclusion

This wraps up our four part series on the Open Source Routing Machine (OSRM). In this last tutorial, we learned how to use the Google Maps Nearby Search API to return a list of nearby restaurants, and how to sort them by distance from our current location using the OSRM Table API. Using these tools, we built a simple restaurant search app that guides the user to the nearest restaurant.

The OSRM APIs offer significant advantages. They are not only highly effective but also free to use if you host them on your own. These APIs are used in the technology stack of many major logistics and ridesharing companies, and provide a robust alternative to well-known services like Google Maps and Mapbox. I hope you found these tutorials as enjoyable and informative to read as I did in writing them.

Part 1: Introduction to OSRM: Setting up osrm-backend using Docker
Part 2: Hosting the OSRM API on EC2: Running osrm-backend as a web service
Part 3: OSRM Route API: Free turn by turn directions and polylines
Part 4: OSRM Table API: Free distance matrix API

👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn