Selecting roads on map with Overpass API

Example of a road selected on map
For a recent project at one of my courses, we had a requirement to highlight a road segment on a map when user clicks on it.
In a search for a ready-to-use database or API with road coordinates, I’ve found Overpass.
It’s a part of an OpenStreetMap project that provides OSM map data over API, and has a feature to get roads around given point, which is exactly what was needed.
You can try the demo right away or proceed reading the article on implementation details.

Overpass uses quite sophisticated query language, which comes in 2 formats: XML and Overpass QL.
You probably couldn’t avoid reading the docs thoroughly to build a complex query, but luckily I’ve found a relevant example on stackoverflow. I converted that example from XML to QL format here and after a few modifications finally got a query I need.

Now to actually run it from our node/express application, we used a query-overpass library.
There’re similar solutions for other languages, for example PHP.
Query-overpass exports just one function and is quite simple by itself, but, like many of your node libraries, it brings a few dependencies for converting OSM data to GeoJSON.
I’m not sure if it can be used in a browser. In our case, we have this functionality on a backend, which is probably a good place for it anyway, because in a real application you would want to query a local database or cache first before making a remote request.

The core function that performs an Overpass query for us looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const overpass = require('query-overpass');
const getNearestRoad = (lat, lon) => {
const maxDist = 10; // maximum distance from the point in meters
const query = `[out:json];
way
(around:${maxDist},${lat},${lon})
["highway"];
(
._;
>;
);
out;`;
return new Promise((resolve, reject) => {
overpass(query, (error, roads) => {
if (error) {
return reject(error);
} else if (roads.features.length < 1) {
return reject({statusCode: 404, message: "No roads found"});
} else {
// we are interested in a single nearest road,
// but an array of roads also can be returned
return resolve(roads.features[0]);
}
});
});
};

As a result, you will have an object in GeoJSON format with a structure like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"type": "Feature",
"id": "way\/423501050",
"properties": {
"type": "way",
"id": 423501050,
"tags": {
"cycleway": "no",
"highway": "secondary",
"maxspeed": "25 mph",
"name": "Middlefield Road",
"tiger:cfcc": "A41",
"tiger:county": "Santa Clara, CA",
"tiger:name_base": "Middlefield",
"tiger:name_type": "Rd",
"tiger:zip_left": "94303",
"tiger:zip_right": "94303"
},
"relations": [
],
"meta": {
}
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-122.1593238,
37.4523135
],
...
]
}
}

Notice it contains not only polygon coordinates, but a whole bunch of other useful information, like street name and max speed.

For user interface, I used Leaflet library with a Polyline representing a road.
You can see the demo below or try it fullscreen here. Click on any road and see what happens.

While this solution feels quick and easy, it’s far from ideal and definitely requires additional improvements.
These are a few problems that I’ve faced and still didn’t solve:

  • The length of returned road segments is unpredictable, and they do not correspond to “official” road boundaries. Some road segments are really long, and some are short.
    Ideally, we’d like to select just a segment between two crossings. While it’s possible to craft an algorithm to slice road segments into smaller ones at the points that intersect with other roads, it would be nice to have this feaure out of the box.
  • For OSM, every road is a polyline, while in reality it’s closer to a polygon. So it’s not possible to plausibly represent a road width and separate lanes on it.
  • Additional properties such as street names, max speed, or number of lanes are not returned for every road. Obviously, it depends on the quality of map data coverage in a given area.
  • For a larger scale, querying a slow rate-limited API is not a good choice. There’s an option to download OSM data, although I didn’t investigate it yet.

You can find a repository with both frontend and backend code for this example on my github.