Data preparation guide

Data preparation guide

This guide explains how to prepare your data for use with GEM, including the required schema, code examples, validation techniques, and best practices.

GEM supports two matching modes, each with its own input and output schema. Choose the section that matches your use case:

Matching modeUse it to…InputSection
Road matchingMatch road segments to GERS IDsParquet with id, is_navigable, geometryRoad matching
Lane-level matchingMatch GPS traces to individual lanesParquet or CSV tracesLane-level matching

Road matching

Road matching takes road segments and matches them to the road network, returning a GERS ID per segment.

Data format requirements

Road matching requires input data in Apache Parquet format with a specific schema.

Required schema

FieldTypeDescriptionExample
idinteger or stringUnique identifier for each road segment5707295
is_navigablebooleanWhether the road is navigable by vehiclestrue
geometrystringRoad geometry in WKT LineString format"LINESTRING (145.18 -37.87, 145.18 -37.87)"

Geometry format

The geometry field must contain valid Well-Known Text (WKT) LineString geometries:

LINESTRING (longitude1 latitude1, longitude2 latitude2, ...)

Examples:

LINESTRING (145.18156 -37.87340, 145.18092 -37.87356)
LINESTRING (4.8952 52.3702, 4.8960 52.3710, 4.8975 52.3725)

Creating Parquet files

Using Python (pandas + pyarrow)

1import pandas as pd
2import pyarrow as pa
3import pyarrow.parquet as pq
4
5# Create sample data
6data = {
7 'id': [1, 2, 3, 4, 5],
8 'is_navigable': [True, True, False, True, True],
9 'geometry': [
10 'LINESTRING (4.8952 52.3702, 4.8960 52.3710)',
11 'LINESTRING (4.8960 52.3710, 4.8975 52.3725)',
12 'LINESTRING (4.8975 52.3725, 4.8990 52.3740)',
13 'LINESTRING (4.8990 52.3740, 4.9005 52.3755)',
14 'LINESTRING (4.9005 52.3755, 4.9020 52.3770)'
15 ]
16}
17
18# Create DataFrame
19df = pd.DataFrame(data)
20
21# Define schema with correct types
22schema = pa.schema([
23 ('id', pa.int64()),
24 ('is_navigable', pa.bool_()),
25 ('geometry', pa.string())
26])
27
28# Convert to PyArrow Table with schema
29table = pa.Table.from_pandas(df, schema=schema)
30
31# Write to Parquet
32pq.write_table(table, 'my_road_data.parquet')
33
34print(f"Created Parquet file with {len(df)} records")

Using Python (GeoPandas)

If your data is already in a geospatial format (Shapefile, GeoJSON, etc.):

1import geopandas as gpd
2import pandas as pd
3
4# Read source data
5gdf = gpd.read_file('roads.shp')
6
7# Prepare for GEM
8gem_data = pd.DataFrame({
9 'id': range(1, len(gdf) + 1), # Generate unique IDs
10 'is_navigable': gdf['navigable'].fillna(True), # Default to True
11 'geometry': gdf.geometry.apply(lambda g: g.wkt) # Convert to WKT
12})
13
14# Filter to LineStrings only
15gem_data = gem_data[gem_data['geometry'].str.startswith('LINESTRING')]
16
17# Save as Parquet
18gem_data.to_parquet('gem_input.parquet', index=False)
19
20print(f"Exported {len(gem_data)} road segments")

Using PySpark

For large datasets:

1from pyspark.sql import SparkSession
2from pyspark.sql.types import StructType, StructField, LongType, BooleanType, StringType
3
4# Initialize Spark
5spark = SparkSession.builder.appName("GEM Data Prep").getOrCreate()
6
7# Define schema
8schema = StructType([
9 StructField("id", LongType(), False),
10 StructField("is_navigable", BooleanType(), False),
11 StructField("geometry", StringType(), False)
12])
13
14# Read your source data
15source_df = spark.read.format("your_format").load("your_data")
16
17# Transform to GEM schema
18gem_df = source_df.select(
19 source_df["road_id"].alias("id"),
20 source_df["navigable"].alias("is_navigable"),
21 source_df["wkt_geometry"].alias("geometry")
22)
23
24# Write as Parquet
25gem_df.write.parquet("gem_input.parquet")

Data validation

Always validate your data before uploading to GEM.

Python validation script

1import pandas as pd
2import re
3
4def validate_gem_data(filepath):
5 """Validate a Parquet file for GEM compatibility."""
6
7 print(f"Validating: {filepath}")
8 errors = []
9 warnings = []
10
11 # Read the file
12 try:
13 df = pd.read_parquet(filepath)
14 except Exception as e:
15 return [f"Cannot read file: {e}"], []
16
17 print(f"Total records: {len(df)}")
18
19 # Check required columns
20 required_cols = ['id', 'is_navigable', 'geometry']
21 missing_cols = [c for c in required_cols if c not in df.columns]
22 if missing_cols:
23 errors.append(f"Missing required columns: {missing_cols}")
24 return errors, warnings
25
26 # Check for null values
27 for col in required_cols:
28 null_count = df[col].isnull().sum()
29 if null_count > 0:
30 errors.append(f"Column '{col}' has {null_count} null values")
31
32 # Check ID uniqueness
33 duplicate_ids = df['id'].duplicated().sum()
34 if duplicate_ids > 0:
35 errors.append(f"Found {duplicate_ids} duplicate IDs")
36
37 # Check data types
38 if not pd.api.types.is_integer_dtype(df['id']):
39 errors.append(f"Column 'id' should be integer, got {df['id'].dtype}")
40
41 if not pd.api.types.is_bool_dtype(df['is_navigable']):
42 errors.append(f"Column 'is_navigable' should be boolean, got {df['is_navigable'].dtype}")
43
44 # Validate geometries
45 linestring_pattern = r'^LINESTRING\s*\([^)]+\)$'
46 invalid_geom = 0
47 for idx, geom in df['geometry'].items():
48 if not isinstance(geom, str):
49 invalid_geom += 1
50 elif not re.match(linestring_pattern, geom.strip(), re.IGNORECASE):
51 invalid_geom += 1
52
53 if invalid_geom > 0:
54 errors.append(f"Found {invalid_geom} invalid geometries (must be WKT LINESTRING)")
55
56 # Check for empty geometries
57 empty_geom = df['geometry'].str.contains(r'LINESTRING\s*\(\s*\)', case=False, regex=True).sum()
58 if empty_geom > 0:
59 warnings.append(f"Found {empty_geom} empty geometries")
60
61 # Summary
62 print(f"\nValidation Results:")
63 print(f" Errors: {len(errors)}")
64 print(f" Warnings: {len(warnings)}")
65
66 if errors:
67 print("\nErrors:")
68 for e in errors:
69 print(f" ❌ {e}")
70
71 if warnings:
72 print("\nWarnings:")
73 for w in warnings:
74 print(f" ⚠️ {w}")
75
76 if not errors:
77 print("\n✅ File is valid for GEM!")
78
79 return errors, warnings
80
81# Usage
82errors, warnings = validate_gem_data('my_road_data.parquet')

Common data quality issues

Issue 1: Invalid geometry format

Problem: Geometries not in WKT LineString format.

Solution:

1from shapely import wkt
2from shapely.geometry import LineString
3
4def fix_geometry(geom):
5 """Convert various geometry formats to WKT LineString."""
6 try:
7 # If it's already a valid WKT string
8 parsed = wkt.loads(geom)
9 if isinstance(parsed, LineString):
10 return geom
11 else:
12 return None # Not a LineString
13 except:
14 return None
15
16df['geometry'] = df['geometry'].apply(fix_geometry)
17df = df.dropna(subset=['geometry'])

Issue 2: Duplicate IDs

Problem: Multiple records share the same ID.

Solution:

1# Option 1: Keep first occurrence
2df = df.drop_duplicates(subset=['id'], keep='first')
3
4# Option 2: Regenerate IDs
5df['id'] = range(1, len(df) + 1)

Issue 3: Mixed geometry types

Problem: Dataset contains Points, Polygons, etc. alongside LineStrings.

Solution:

# Filter to LineStrings only
df = df[df['geometry'].str.upper().str.startswith('LINESTRING')]

Issue 4: Coordinate system issues

Problem: Coordinates in wrong order or projection.

Solution:

1import geopandas as gpd
2from shapely import wkt
3
4# Read and reproject
5gdf = gpd.read_file('roads.shp')
6gdf = gdf.to_crs('EPSG:4326') # Convert to WGS84
7
8# Extract WKT
9df['geometry'] = gdf.geometry.apply(lambda g: g.wkt)

Best practices

Before uploading

  1. Start small: Test with a subset (1,000-10,000 records) before processing full dataset
  2. Validate thoroughly: Run validation script on every file
  3. Check file size: Large files may take longer to upload; plan accordingly
  4. Use descriptive filenames: city_roads_2024_v1.parquet not data.parquet

Data quality tips

  1. Clean geometries: Remove self-intersections and invalid geometries
  2. Ensure connectivity: Connected road networks match better than isolated segments
  3. Include all segments: Don't filter out small roads—they help with context
  4. Accurate navigability: Set is_navigable correctly for better matching

File naming conventions

Recommended naming pattern:

{region}_{data_type}_{date}_{version}.parquet

Examples:

  • netherlands_roads_20240115_v1.parquet
  • california_highways_20240120_v2.parquet
  • tokyo_streets_20240118_final.parquet

Sample data

Here's a minimal sample file you can use for testing:

1import pandas as pd
2
3# Sample Amsterdam road segments
4sample_data = {
5 'id': [1, 2, 3, 4, 5],
6 'is_navigable': [True, True, True, True, False],
7 'geometry': [
8 'LINESTRING (4.8952 52.3702, 4.8960 52.3710)',
9 'LINESTRING (4.8960 52.3710, 4.8975 52.3725)',
10 'LINESTRING (4.8975 52.3725, 4.8990 52.3740)',
11 'LINESTRING (4.8990 52.3740, 4.9005 52.3755)',
12 'LINESTRING (4.9005 52.3755, 4.9020 52.3770)'
13 ]
14}
15
16df = pd.DataFrame(sample_data)
17df.to_parquet('sample_gem_input.parquet', index=False)
18print("Sample file created: sample_gem_input.parquet")

Output data schema

FieldTypeDescription
idstring or integerYour original road segment ID
gersstringMatched GERS ID (UUID format)
confidenceintegerMatch confidence score (0-100)
lr_idstringLinear reference: coordinates and GERS ID
lr_gersstringLinear reference: distance range and original ID

Example:

{"id":"abc","gers":"550e8400-e29b-41d4-a716-446655440000","confidence":99,"lr_id":"52.0197-76.36744#550e8400-e29b-41d4-a716-446655440000","lr_gers":"0.0-100.0#abc"}

Lane-level matching

Lane-level matching takes GPS traces as input and matches each trace to the road network, producing per-lane matches with geometry and confidence.

Traces can be supplied in two formats, selected automatically from the file extension:

  • Parquet (.parquet) — trace records in the trace schema below. This is the native format and is processed directly.
  • CSV (.csv) — converted to the same Trace Parquet records on ingest, then processed identically.
1Parquet traces ──────────────────────────────┐
2 ├─→ prepare → match → pack → results.parquet
3CSV traces (convert to Parquet traces) ─────┘

Both paths converge on the same Trace schema, so the schema below is what your Parquet file must contain — and what a CSV is converted into. Uploaded files must end in .parquet or .csv; the results are written as <input-name>.results.parquet.

Trace schema

A trace is a Trace record containing an ordered list of TracePoints. This is the logical model behind both the Parquet and CSV inputs.

Trace

FieldTypeRequiredDescription
idstringYesUnique trace identifier
pointsarray of TracePointYesOrdered sequence of trace points

TracePoint

FieldTypeRequiredDescription
coordCoordYesPoint coordinates
timestamplongYesUnix timestamp (ms) or sequential index
headingdoubleYesBearing in degrees, 0360
velocitydoubleYesVelocity (m/s); 0.0 when unknown
lanelongNoPre-assigned lane id, if available (nullable)

Coord

FieldTypeRequiredDescription
xdoubleYesLongitude (WGS84)
ydoubleYesLatitude (WGS84)

Parquet input

Parquet is the native input format. Each row is one Trace, with points stored as an array of structs and coord as a nested struct — so the schema is nested, not flat.

Parquet schema

1root
2 |-- id: string (nullable = true)
3 |-- points: array (nullable = true)
4 | |-- element: struct (containsNull = true)
5 | | |-- coord: struct (nullable = true)
6 | | | |-- x: double (nullable = false) # longitude (WGS84)
7 | | | |-- y: double (nullable = false) # latitude (WGS84)
8 | | |-- heading: double (nullable = false) # degrees, 0360
9 | | |-- lane: long (nullable = true) # optional pre-assigned lane id
10 | | |-- timestamp: long (nullable = false) # ms or sequential index
11 | | |-- velocity: double (nullable = false) # m/s, 0.0 if unknown

Field order within a struct is not significant — fields are read by name. What matters is the nesting, the names, and the types.

Creating a Parquet trace file (PySpark)

Building the nested structure is easiest with Spark, which is also what the pipeline uses internally:

1from pyspark.sql import SparkSession
2from pyspark.sql.types import (
3 StructType, StructField, StringType, DoubleType, LongType, ArrayType
4)
5
6spark = SparkSession.builder.appName("GEM Trace Prep").getOrCreate()
7
8coord = StructType([
9 StructField("x", DoubleType(), False), # longitude
10 StructField("y", DoubleType(), False), # latitude
11])
12
13trace_point = StructType([
14 StructField("coord", coord, True),
15 StructField("heading", DoubleType(), False),
16 StructField("lane", LongType(), True),
17 StructField("timestamp", LongType(), False),
18 StructField("velocity", DoubleType(), False),
19])
20
21trace_schema = StructType([
22 StructField("id", StringType(), True),
23 StructField("points", ArrayType(trace_point, True), True),
24])
25
26rows = [
27 ("trace-001", [
28 {"coord": {"x": 4.8952, "y": 52.3702}, "heading": 34.2, "lane": None, "timestamp": 0, "velocity": 0.0},
29 {"coord": {"x": 4.8960, "y": 52.3710}, "heading": 34.2, "lane": None, "timestamp": 1, "velocity": 0.0},
30 ]),
31]
32
33spark.createDataFrame(rows, trace_schema).write.mode("overwrite").parquet("traces.parquet")

Note: pandas + PyArrow can also write this file, but you must build points as a list of structs and coord as a nested struct using pyarrow.struct(...) / pyarrow.list_(...) types — a flat table will be rejected.

CSV input

When you upload a .csv file, the pipeline converts each row into the Trace schema above. Two CSV formats are supported, detected automatically from the CSV column headers.

FormatRequired columnsGeometryResulting trace
Road profile (road_category, road_waviness)uuid, shapeWKT LINESTRINGMulti-point trace — one point per vertex
Road events (road_events)uuid, heading, coordinateWKT POINTSingle-point trace

Road profile format

Use this format when each trace is a polyline (a driven path or road profile).

ColumnTypeDescriptionExample
uuidstringUnique trace identifier → Trace.idtrace-001
shapestringTrace geometry in WKT LINESTRING format"LINESTRING (4.8952 52.3702, 4.8960 52.3710)"

For each vertex in shape, one TracePoint is created:

  • The heading is computed automatically from consecutive vertices (bearing in degrees, 0360).
  • The timestamp is set to the vertex index (0, 1, 2, …).
  • The velocity is set to 0.0.
  • The lane is left unset (null).

Road events format

Use this format when each trace is a single point with a known heading (for example, a road event observation).

ColumnTypeDescriptionExample
uuidstringUnique trace identifier → Trace.idevent-001
headingnumberHeading/bearing in degrees, 036092.5
coordinatestringPoint geometry in WKT POINT format"POINT (4.8952 52.3702)"

Each row produces a single-point trace. The timestamp and velocity are set to 0.

Geometry format

CSV geometries use Well-Known Text (WKT) with longitude first, latitude second (WGS84 / EPSG:4326):

LINESTRING (longitude1 latitude1, longitude2 latitude2, ...)
POINT (longitude latitude)

Examples:

1LINESTRING (145.18156 -37.87340, 145.18092 -37.87356)
2LINESTRING (4.8952 52.3702, 4.8960 52.3710, 4.8975 52.3725)
3POINT (4.8952 52.3702)

Creating CSV files

Road profile CSV:

1import csv
2
3# Trace polylines as WKT LINESTRINGs
4traces = [
5 ("trace-001", "LINESTRING (4.8952 52.3702, 4.8960 52.3710, 4.8975 52.3725)"),
6 ("trace-002", "LINESTRING (4.8990 52.3740, 4.9005 52.3755, 4.9020 52.3770)"),
7]
8
9with open("road_profile.csv", "w", newline="") as f:
10 writer = csv.writer(f)
11 writer.writerow(["uuid", "shape"])
12 writer.writerows(traces)
13
14print(f"Wrote {len(traces)} traces")

Road events CSV:

1import csv
2
3# Single-point events: (uuid, heading, WKT POINT)
4events = [
5 ("event-001", 92.5, "POINT (4.8952 52.3702)"),
6 ("event-002", 180.0, "POINT (4.8990 52.3740)"),
7]
8
9with open("road_events.csv", "w", newline="") as f:
10 writer = csv.writer(f)
11 writer.writerow(["uuid", "heading", "coordinate"])
12 writer.writerows(events)
13
14print(f"Wrote {len(events)} events")

Converting geospatial data to a road profile CSV:

1import geopandas as gpd
2import pandas as pd
3
4# Read and reproject to WGS84
5gdf = gpd.read_file("roads.shp").to_crs("EPSG:4326")
6
7# Keep LineStrings only and export the road profile columns
8profile = pd.DataFrame({
9 "uuid": [f"trace-{i}" for i in range(len(gdf))],
10 "shape": gdf.geometry.apply(lambda g: g.wkt),
11})
12profile = profile[profile["shape"].str.upper().str.startswith("LINESTRING")]
13
14profile.to_csv("road_profile.csv", index=False)
15print(f"Exported {len(profile)} traces")

Data validation

Validating a Parquet trace file (PySpark)

Read the file and confirm the nested schema and that no trace is empty:

1from pyspark.sql import functions as F
2
3df = spark.read.parquet("traces.parquet")
4df.printSchema() # Compare against the Parquet schema above
5
6assert "id" in df.columns and "points" in df.columns, "Missing id/points columns"
7
8problems = df.filter(
9 F.col("id").isNull() | (F.size("points") == 0)
10).count()
11print("OK" if problems == 0 else f"{problems} traces with null id or no points")

Validating a CSV file

1import csv
2import re
3
4LINESTRING_RE = re.compile(r"^LINESTRING\s*\([^)]+\)$", re.IGNORECASE)
5POINT_RE = re.compile(r"^POINT\s*\([^)]+\)$", re.IGNORECASE)
6
7def validate_traces(filepath):
8 """Validate a road profile or road events CSV for GEM lane matching."""
9 errors, warnings = [], []
10
11 with open(filepath, newline="") as f:
12 reader = csv.DictReader(f)
13 columns = set(reader.fieldnames or [])
14
15 # Detect format from headers
16 if {"uuid", "shape"}.issubset(columns):
17 fmt, geom_col, geom_re = "road_profile", "shape", LINESTRING_RE
18 elif {"uuid", "heading", "coordinate"}.issubset(columns):
19 fmt, geom_col, geom_re = "road_events", "coordinate", POINT_RE
20 else:
21 return [f"Unrecognized columns: {sorted(columns)}"], []
22
23 print(f"Detected format: {fmt}")
24
25 seen_ids = set()
26 for line_no, row in enumerate(reader, start=2):
27 uuid = (row.get("uuid") or "").strip()
28 if not uuid:
29 errors.append(f"Line {line_no}: missing uuid")
30 elif uuid in seen_ids:
31 errors.append(f"Line {line_no}: duplicate uuid '{uuid}'")
32 else:
33 seen_ids.add(uuid)
34
35 geom = (row.get(geom_col) or "").strip()
36 if not geom_re.match(geom):
37 errors.append(f"Line {line_no}: invalid {geom_col} geometry")
38
39 if fmt == "road_events":
40 heading = (row.get("heading") or "").strip()
41 try:
42 value = float(heading)
43 if not 0.0 <= value < 360.0:
44 warnings.append(f"Line {line_no}: heading {value} outside [0, 360)")
45 except ValueError:
46 errors.append(f"Line {line_no}: heading '{heading}' is not a number")
47
48 print(f"\nErrors: {len(errors)} Warnings: {len(warnings)}")
49 for error in errors:
50 print(f" ❌ {error}")
51 for warning in warnings:
52 print(f" ⚠️ {warning}")
53 if not errors:
54 print("\n✅ File is valid for GEM lane matching!")
55
56 return errors, warnings
57
58# Usage
59errors, warnings = validate_traces("road_profile.csv")

Common data quality issues

Issue 1: Flat Parquet instead of nested

Problem: The Parquet file has flat columns (for example x, y, heading per row) instead of a points array of structs.

Solution: Group your points per trace and build the nested points array as shown in Creating a Parquet trace file. Each row must be a whole trace, not a single point.

Issue 2: Invalid geometry format (CSV)

Problem: shape/coordinate values are not valid WKT.

Solution:

1from shapely import wkt
2from shapely.geometry import LineString, Point
3
4def is_valid(geom, expected):
5 try:
6 return isinstance(wkt.loads(geom), expected)
7 except Exception:
8 return False
9
10# Road profile
11df = df[df["shape"].apply(lambda g: is_valid(g, LineString))]
12# Road events
13df = df[df["coordinate"].apply(lambda g: is_valid(g, Point))]

Issue 3: Duplicate trace IDs

Problem: Multiple traces share the same id (uuid).

Solution:

# CSV: keep the first occurrence
df = df.drop_duplicates(subset=["uuid"], keep="first")

Issue 4: Wrong coordinate order or projection

Problem: Coordinates are in latitude/longitude order or a non-WGS84 projection. Coordinates must be longitude first, in EPSG:4326.

Solution:

1import geopandas as gpd
2
3gdf = gpd.read_file("roads.shp").to_crs("EPSG:4326") # Reproject to WGS84
4df["shape"] = gdf.geometry.apply(lambda g: g.wkt) # lon lat order

Best practices

Before running

  1. Prefer Parquet for repeated or large runs: it skips the CSV conversion step and is the native format.
  2. Start small: Test with a subset (1,000–10,000 traces) before processing the full dataset.
  3. Validate thoroughly: Check the schema (Parquet) or headers (CSV) on every file.
  4. Use one CSV format per file: Do not mix road profile and road events columns in a single CSV.
  5. Use descriptive filenames: netherlands_traces_20240115_v1.parquet, not data.parquet.

Data quality tips

  1. Clean geometries: Remove empty, self-intersecting, or degenerate geometries.
  2. Consistent WGS84: Keep all coordinates in EPSG:4326 with longitude first.
  3. Meaningful headings: For road events, provide accurate headings — they drive directional matching.
  4. Stable IDs: Keep trace ids stable across runs so you can join matches back to your source data.

Output data schema

Each input trace produces one Match. A match contains the matched road segments; each road segment optionally carries lane-level details.

Match

FieldTypeDescription
traceIdstringThe id of the input trace
roadsarray of RoadMatchMatched road segments (empty if no route was found)

RoadMatch

FieldTypeDescription
roadAreaIdstringIdentifier of the matched road area
startConnectorIdstringConnector id at the start of the matched segment
endConnectorIdstringConnector id at the end of the matched segment
laneLaneMatchLane-level match details (nullable)

LaneMatch

FieldTypeDescription
laneIdstringIdentifier of the matched lane
wktstringLane geometry as a WKT LINESTRING
confidencedoubleLane match confidence score
startOffsetdoubleOffset along the road to the start of the lane match (nullable)
endOffsetdoubleOffset along the road to the end of the lane match (nullable)

Output Parquet schema

The results file (<input-name>.results.parquet) is nested, mirroring the model above:

1root
2 |-- roads: array (nullable = true)
3 | |-- element: struct (containsNull = true)
4 | | |-- endConnectorId: string (nullable = true)
5 | | |-- lane: struct (nullable = true)
6 | | | |-- confidence: double (nullable = false)
7 | | | |-- endOffset: double (nullable = true)
8 | | | |-- laneId: string (nullable = true)
9 | | | |-- startOffset: double (nullable = true)
10 | | | |-- wkt: string (nullable = true)
11 | | |-- roadAreaId: string (nullable = true)
12 | | |-- startConnectorId: string (nullable = true)
13 |-- traceId: string (nullable = true)

Example (one match, shown as JSON):

1{
2 "traceId": "trace-001",
3 "roads": [
4 {
5 "roadAreaId": "12345:678:90",
6 "startConnectorId": "12345:678:11",
7 "endConnectorId": "12345:678:12",
8 "lane": {
9 "laneId": "12345:678:42",
10 "wkt": "LINESTRING (4.8952 52.3702, 4.8960 52.3710)",
11 "confidence": 100.0,
12 "startOffset": 0.0,
13 "endOffset": 37.5
14 }
15 }
16 ]
17}

Note: The lane field is null when only road-level matching was possible. When no route is found for a trace, roads is an empty list.


Next steps

Once your data is prepared and validated: