Road matching data
Request accessRoad matching data
Road matching (ROAD_MATCHING) matches your road segments to the road network of Overture Maps. For every match it returns the GERS ID of the matched Overture segment and the matched parts of both lines.
At a glance
| Input | Results |
|---|---|
One .parquet file, one row per road segment | One .parquet file, one row per match |
Required columns: id, geometry | Columns: id, gers, confidence, lr_id, lr_gers |
Optional column: is_navigable | Optional GeoJSON visualization data |
Input columns
| Column | Type | Required | Rules | Example |
|---|---|---|---|---|
id | integer (32- or 64-bit) or string | Yes | Unique for every row. The results return it as a string. | 5707295 |
geometry | string | Yes | Road geometry as WKT LINESTRING in WGS84 (EPSG:4326), longitude first | LINESTRING (4.8952 52.3702, 4.8960 52.3710) |
is_navigable | boolean | No | true when vehicles can drive on the road. Without this column, GEM treats all roads as navigable. An empty value counts as not navigable. | true |
Your file can contain other columns as well.
Example input
| id | geometry | is_navigable |
|---|---|---|
5707295 | LINESTRING (4.8952 52.3702, 4.8960 52.3710) | true |
5707296 | LINESTRING (4.8960 52.3710, 4.8975 52.3725) | true |
5707297 | LINESTRING (4.8975 52.3725, 4.8990 52.3740) | false |
Checks GEM runs
When a job starts, GEM checks the input file in the order below. If a check fails, the job ends with status FAILED, and the message appears on the job details page in the UI and in error_messages in the API. In the messages, <filename> is the file name without its folder.
| Check | Message | Fix |
|---|---|---|
| The file exists | Input file '<filename>' was not found in the storage location. | Re-upload the file and submit a new job |
| The file is a valid Parquet file | Input file '<filename>' is not a valid Parquet file. | Check the file with a Parquet tool and re-upload it |
| The required columns are present | Input file '<filename>' is missing required column(s): '<column>'. Required columns: 'id', 'geometry'. | Add the columns, see Input columns |
| The columns have supported types | Column '<column>' in input file '<filename>' has unsupported type '<type>'; expected 'integer' or 'string'. | Convert the column, see Common problems |
| The IDs are unique | Column 'id' in input file '<filename>' contains <N> duplicated value(s). Each road must have a unique 'id'. | Remove or renumber the duplicated IDs |
| The geometries are valid | Column 'geometry' in input file '<filename>' contains <N> value(s) that are not valid WKT LINESTRING geometries. | Make every geometry value a valid WKT LINESTRING string |
The type message names the type the column needs: 'integer' or 'string' for id, 'string' for geometry, and 'boolean' for is_navigable. GEM reports missing columns and unsupported types together, and duplicated IDs and invalid geometries together.
To find these problems before you upload the file, run the validation script.
Create the file
Write your data to a single Parquet file. Keep your own road IDs, so you can join the results back to your data.
From a table (pandas)
import pandas as pdimport pyarrow as paimport pyarrow.parquet as pq
roads = pd.DataFrame({ "id": [5707295, 5707296, 5707297], "geometry": [ "LINESTRING (4.8952 52.3702, 4.8960 52.3710)", "LINESTRING (4.8960 52.3710, 4.8975 52.3725)", "LINESTRING (4.8975 52.3725, 4.8990 52.3740)", ], "is_navigable": [True, True, False],})
schema = pa.schema([("id", pa.int64()), ("geometry", pa.string()), ("is_navigable", pa.bool_())])pq.write_table(pa.Table.from_pandas(roads, schema=schema, preserve_index=False), "roads.parquet")From GIS data (GeoPandas)
GeoPandas reads Shapefile, GeoJSON, GeoPackage, and other GIS formats. GEM needs geometries as WKT text, so convert them with to_wkt() and write the file with pandas. GeoDataFrame.to_parquet stores geometries as WKB binary instead.
import geopandas as gpdimport pandas as pd
roads = gpd.read_file("roads.shp").to_crs("EPSG:4326").explode(index_parts=False)roads = roads[roads.geom_type == "LineString"].reset_index(drop=True)
part = roads.groupby("road_id").cumcount()road_id = roads["road_id"].astype(str)
pd.DataFrame({ "id": road_id.where(part == 0, road_id + "-" + part.astype(str)), "geometry": roads.geometry.to_wkt(),}).to_parquet("roads.parquet", index=False)Replace road_id with the ID column of your data. Multi-part lines become one row per part, and every part after the first gets a -1, -2, … suffix. If your data says which roads are navigable, add it as the is_navigable column.
Large datasets (PySpark)
Spark writes a folder of part files. Write one partition and upload its part file under the name you want to use in GEM:
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("gem-road-input").getOrCreate()
roads = spark.read.parquet("source_roads").select( F.col("road_id").alias("id"), F.col("wkt").alias("geometry"), F.col("navigable").cast("boolean").alias("is_navigable"),)roads.coalesce(1).write.mode("overwrite").parquet("roads_spark")az storage blob upload --account-name "<STORAGE_ACCOUNT_NAME>" \ --container-name "default" \ --name "roads.parquet" \ --file "$(ls roads_spark/part-*.parquet)" \ --auth-mode loginValidate before upload
This script runs the same checks as GEM and prints the same messages. It also warns about problems that lead to poor matches. It needs Python 3 with pyarrow and shapely:
pip install pyarrow shapelypython validate_road_input.py roads.parquet"""Check a road matching input file before uploading it to GEM.
Usage: python validate_road_input.py <file.parquet>"""import sysfrom pathlib import Path
import numpy as npimport pyarrow as paimport pyarrow.compute as pcimport pyarrow.parquet as pqimport shapely
REQUIRED_COLUMNS = ["id", "geometry"]EXPECTED_TYPES = { "id": ([pa.int32(), pa.int64(), pa.string(), pa.large_string()], "'integer' or 'string'"), "geometry": ([pa.string(), pa.large_string()], "'string'"), "is_navigable": ([pa.bool_()], "'boolean'"),}TYPE_NAMES = {"int8": "tinyint", "int16": "smallint", "int32": "int", "int64": "bigint", "bool": "boolean", "large_string": "string", "large_binary": "binary"}LINE_TYPES = [shapely.GeometryType.LINESTRING, shapely.GeometryType.LINEARRING]
def quoted(names): return ", ".join(f"'{name}'" for name in names)
def schema_errors(schema, name): errors = [] missing = [column for column in REQUIRED_COLUMNS if column not in schema.names] if missing: errors.append(f"Input file '{name}' is missing required column(s): {quoted(missing)}. " f"Required columns: {quoted(REQUIRED_COLUMNS)}.") for column, (allowed, expected) in EXPECTED_TYPES.items(): if column in schema.names and schema.field(column).type not in allowed: actual = str(schema.field(column).type) errors.append(f"Column '{column}' in input file '{name}' has unsupported type " f"'{TYPE_NAMES.get(actual, actual)}'; expected {expected}.") return errors
def data_findings(path, name): errors, warnings = [], [] ids = pq.read_table(path, columns=["id"]).column("id").combine_chunks() duplicated = pc.sum(pc.greater(pc.value_counts(ids).field("counts"), 1)).as_py() or 0 if duplicated: errors.append(f"Column 'id' in input file '{name}' contains {duplicated} duplicated value(s). " "Each road must have a unique 'id'.") if ids.null_count: warnings.append(f"Column 'id' has {ids.null_count} empty value(s). Give every road an ID.")
invalid = empty = outside = 0 for batch in pq.ParquetFile(path).iter_batches(batch_size=100_000, columns=["geometry"]): geometries = shapely.from_wkt(batch.column(0).to_numpy(zero_copy_only=False), on_invalid="ignore") lines = np.isin(shapely.get_type_id(geometries), LINE_TYPES) invalid += int((~lines).sum()) empty += int((lines & shapely.is_empty(geometries)).sum()) bounds = shapely.bounds(geometries[lines]) outside += int(((np.abs(bounds[:, [0, 2]]) > 180) | (np.abs(bounds[:, [1, 3]]) > 90)).any(axis=1).sum()) if invalid: errors.append(f"Column 'geometry' in input file '{name}' contains {invalid} value(s) " "that are not valid WKT LINESTRING geometries.") if empty: warnings.append(f"Column 'geometry' has {empty} empty LINESTRING value(s), which can't be matched.") if outside: warnings.append(f"Column 'geometry' has {outside} value(s) with coordinates outside the " "longitude/latitude range. Use WGS84 (EPSG:4326) with longitude first.") return errors, warnings
def main(file_name): path = Path(file_name) if not path.is_file(): print(f"ERROR: {path} does not exist.") return 1 try: schema = pq.read_schema(path) except pa.ArrowInvalid: print(f"ERROR: Input file '{path.name}' is not a valid Parquet file.") return 1
errors, warnings = schema_errors(schema, path.name), [] if not errors: errors, warnings = data_findings(path, path.name) if "is_navigable" not in schema.names: warnings.append("Column 'is_navigable' is missing, so GEM treats all roads as navigable.")
for message in errors: print(f"ERROR: {message}") for message in warnings: print(f"WARNING: {message}") if not errors: print(f"OK: {path.name} is ready for road matching.") return 1 if errors else 0
if __name__ == "__main__": if len(sys.argv) != 2: sys.exit(__doc__) sys.exit(main(sys.argv[1]))Example output for a file with problems:
ERROR: Column 'id' in input file 'roads.parquet' contains 2 duplicated value(s). Each road must have a unique 'id'.WARNING: Column 'geometry' has 15 value(s) with coordinates outside the longitude/latitude range. Use WGS84 (EPSG:4326) with longitude first.Results
GEM writes the results to <name>.results.parquet, next to your input file. The file has one row for every match between one of your roads and a segment of the Overture release you selected. A road that matches several segments has several rows. Roads without a match have no rows.
| Column | Type | Description |
|---|---|---|
id | string | ID of your road |
gers | string | GERS ID of the matched Overture segment (UUID format) |
confidence | integer | Match confidence from 0 to 100 |
lr_id | string | Matched part of your road, as <start>-<end>#<gers> |
lr_gers | string | Matched part of the Overture segment, as <start>-<end>#<id> |
Linear references
lr_id and lr_gers describe which part of a line a match covers. <start> and <end> are positions along the line, as percentages of its length measured from its first point. 0.0-100.0 is the whole line. The ID after # names the line on the other side of the match.
Your road 5707296 0% 40% 100% |===============|============================| |---------------| lr_id 0.0-40.0#5f1a9c3b-… |----------------------------| lr_id 40.0-100.0#9d2e71a3-…Example results
Results for the example input could look like this. The values are illustrative.
| id | gers | confidence | lr_id | lr_gers |
|---|---|---|---|---|
5707295 | 0b5c2f4e-8d3a-4c1e-9f6b-2a7d5e8c1b34 | 97 | 0.0-100.0#0b5c2f4e-8d3a-4c1e-9f6b-2a7d5e8c1b34 | 12.5-64.1#5707295 |
5707296 | 5f1a9c3b-7e2d-4b8a-a1c6-3d9e0f4b7a52 | 88 | 0.0-40.0#5f1a9c3b-7e2d-4b8a-a1c6-3d9e0f4b7a52 | 55.0-100.0#5707296 |
5707296 | 9d2e71a3-4c5b-4f0e-8b2d-6a1c3e5f7d90 | 91 | 40.0-100.0#9d2e71a3-4c5b-4f0e-8b2d-6a1c3e5f7d90 | 0.0-38.7#5707296 |
Road 5707295 matches one segment along its whole length. Road 5707296 matches two segments: one for its first 40% and one for the rest. Road 5707297 has no match, so it has no rows.
Read the results
import pandas as pd
results = pd.read_parquet("roads.results.parquet")for column in ("lr_id", "lr_gers"): parts = results[column].str.extract(r"^(?P<start>[\d.]+)-(?P<end>[\d.]+)#(?P<ref>.+)$") results[f"{column}_start"] = parts["start"].astype(float) results[f"{column}_end"] = parts["end"].astype(float)
print(results[["id", "gers", "confidence", "lr_id_start", "lr_id_end"]])Visualization data
When you enable visualization data (with_visualization_data: true in the API, or Generate visualization data in the UI), GEM also writes GeoJSON files for viewing the matches in GIS tools such as QGIS. They are stored next to the results file in a folder named <name>-visualization-data/, where <name> is your input file name up to its first dot. For example, input/roads.parquet produces input/roads-visualization-data/.
| File | Contents |
|---|---|
all-matched.geojson | All matched roads, with the matched parts of the Overture segments |
all-unmatched.geojson | All unmatched roads, with the candidate segments GEM considered |
part_0000.geojson, part_0001.geojson, … | The matched roads, split into files of about 1,000 features and grouped by location |
part_0000-unmatched.geojson, … | The unmatched roads, split the same way |
detailed.zip | One file per road: <id>.geojson for a matched road, <id>-unmatched.geojson for an unmatched road |
Every feature is a LineString with these properties:
| Property | Description |
|---|---|
layer | src for your road, matched for the matched part of an Overture segment, unmatched_candidate for a candidate segment of an unmatched road |
src_id | ID of your road |
trg_id | GERS ID of the Overture segment |
confidence | Match confidence from 0 to 100 |
lr_src_start, lr_src_end | Matched part of your road, in percent |
lr_trg_start, lr_trg_end | Matched part of the Overture segment, in percent |
matched_count, unmatched_candidates, partial_ranges | On src features: the number of matched or candidate segments, and their ranges |
color, line_width, line_style | Styling hints: blue for your roads, shades of red by confidence for segments |
Download the folder with Azure CLI:
az storage blob download-batch --account-name "<STORAGE_ACCOUNT_NAME>" \ --source "default" \ --pattern "<name>-visualization-data/*" \ --destination "/path/to/destination" \ --auth-mode loginCommon problems
| Problem | Fix |
|---|---|
has unsupported type 'binary' for geometry | The file stores geometries as WKB, as GeoDataFrame.to_parquet does. Convert them to WKT text, see From GIS data. |
has unsupported type 'double' for id | In pandas, missing IDs turn an integer column into floating-point numbers. Give every road an ID, or store the IDs as strings. |
not valid WKT LINESTRING geometries | Split MULTILINESTRING values into single lines and remove other geometry types. |
duplicated value(s) | Make the IDs unique, for example with a suffix for repeated IDs, and keep a mapping to your source data. |
| Spark created a folder instead of a file | Upload the single part file, see Large datasets. |
| Roads match poorly or not at all | Check that the coordinates are WGS84 (EPSG:4326) with longitude first. |
Next steps
- Data preparation - File requirements for all matching types
- UI workflow guide - Upload your file and run a job in the UI
- API workflow guide - Upload your file and run a job with the API
- Troubleshooting - Solutions for common problems