OSRM Table API: Free and open source distance matrix API
Learn about the OSRM Table API, a free and open source alternative to the Google Maps 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).
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.
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).
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)
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.
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).
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).
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.
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.
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