OSM Legal Default Speeds

Kotlin multiplatform library that provides information about legal default speed limits. Runs on JVM, native and JavaScript.

It can be used by data consumers such as router software to fill the gaps in OpenStreetMap data, i.e. when explicit maxspeed tagging is missing because there is no explicit sign or because it hasn’t been tagged yet.

Additionally, it can be used to supplement the legal speed limits for other vehicle types (buses, goods vehicles, motorcycles, …), as these can be assumed to never be tagged unless explicitly signed.

It uses the data from the Default speed limits page from the OpenStreetMap wiki as input. In the parser/ subdirectory, there is a parser written in Python that generates a JSON from the data in the aforementioned wiki page. The data from this JSON can be consumed by the library (found in the library/ subdirectory).


Check it out on westnordost.github.io/osm-legal-default-speeds

(Source code for the demo is in the demo/ subdirectory)

Copyright and License

© 2022 Tobias Zwick. This library is released under the terms of the BSD 3-Clause License.


Add de.westnordost:osm-legal-default-speeds:1.0 as a Maven dependency or download the jar from there.


Parse Data

You need to parse the legal_default_speeds.json (see parser/ subdirectory) with the JSON library of your choice and feed its data into the constructor.

Example parsing it with kotlinx-serialization (click to expand)

@Serializable data class SpeedLimitsJson(
    val meta: Map<String, String>,
    val roadTypesByName: Map<String, RoadTypeFilterJson>,
    val speedLimitsByCountryCode: Map<String, List<RoadTypeJson>>,
    val warnings: List<String>

@Serializable data class RoadTypeFilterJson(
    override val filter: String? = null,
    override val fuzzyFilter: String? = null,
    override val relationFilter: String? = null
) : RoadTypeFilter

@Serializable data class RoadTypeJson(
    override val name: String? = null,
    override val tags: Map<String, String>
) : RoadType

val data: SpeedLimitsJson = Json.decodeFromStream(defaultSpeedsJsonFile.openStream())
val legalSpeeds = LegalDefaultSpeeds(data.roadTypes, data.speedLimits)

Query speed limits

Specify the ISO 3166-1 alpha 2 code the road segment is located in and its tags.

legalSpeeds.getSpeedLimits("DK", mapOf("highway" to "motorway"))

This returns:

    roadTypeName = "motorway",
    tags = mapOf(
        "maxspeed" to "130",
        "maxspeed:bus:conditional" to "80 @ (maxweightrating>3.5)",
        "maxspeed:coach" to "100",
        "maxspeed:conditional" to "80 @ (trailer); 80 @ (maxweightrating>3.5)",
        "maxspeed:hgv" to "80",
        "minspeed" to "50"
    certitude = Certitude.Exact


Matching by relation membership

Especially in the United States but in some other countries as well, road types can be identified by membership in a (route) relation. So if you have access to the relations each road segment is a member of, you can specify the tags of the relations to improve accuracy.

Tag filters for relations are defined in the fourth column of the road types table in the wiki.


    tags = mapOf("lanes" to "2", "oneway" to "yes"),
    relationsTags = listOf(mapOf("type" to "route", "route" to "road", "network" to "US:I"))


    roadTypeName = "US interstate highway with 2 or more lanes in each direction",
    tags = mapOf("maxspeed" to "75 mph"),
    certitude = Certitude.Exact

Replacing placeholders

Matching for certain properties of a road, such as whether it is urban or not, is done via placeholders. See any “tag” in curly braces in the road types table. These can be replaced, if for example you have another data source with which it is possible to determine a property more precisely.


legalSpeeds.getSpeedLimits("US-MO", mapOf("highway" to "motorway"), null)
{ (name, evaluate) -> 
    if (name == "urban") myDataSource.isUrban(roadSegment) else evaluate()

…returns (if myDataSource.isUrban returns true)…

    roadTypeName = "urban motorway",
    tags = mapOf("maxspeed" to "60 mph"),
    certitude = Certitude.Exact

Matching by given speed limit

When tags of the given road segment already contain a maxspeed value, a “reverse” match by that value is attempted in case no exact match was found.

legalSpeeds.getSpeedLimits("AT", mapOf("maxspeed" to "100"))


    roadTypeName = "rural",
    tags = mapOf(
        "maxspeed:bus" to "80",
        "maxspeed:bus:conditional" to "70 @ (articulated)",
        "maxspeed:conditional" to "80 @ (trailer); 70 @ (maxweightrating>3.5)",
        "maxspeed:hgv" to "70"
    certitude = Certitude.FromMaxSpeed

This only works if the given maxspeed matches exactly the maxspeed of the road type it should match with. Such matching is preferred over fuzzy matches.

The tags returned are always only the tags additional to the ones the road segment already has, so the maxspeed of 100 km/h is omitted in the result.

Fuzzy matching

Which tag filters constitute fuzzy tag matching rules are defined in the third column of the road types table in the wiki.

legalSpeeds.getSpeedLimits("BO", mapOf("highway" to "residential"))


    roadTypeName = "urban",
    tags = mapOf("maxspeed" to "40"),
    certitude = Certitude.Fuzzy

…because roads tagged with highway=residential are oftentimes urban roads.

Fallback to default road type

If nothing matches, the speed limits of the default road type are returned. The default road type is usually the default road outside settlements. Some countries do not have default road types defined, in which case simply null is returned.

legalSpeeds.getSpeedLimits("GD", mapOf())


    roadTypeName = null,
    tags = mapOf(
        "maxspeed" to "40 mph",
        "maxspeed:bus" to "35 mph",
        "maxspeed:goods" to "35 mph"
    certitude = Certitude.Fallback


This library was made possible with a grant from NLNet Zero Discovery.


View Github