AccessScore: Intersections API #
Experimental. There is no single correct way to measure whether an intersection is crossable, and this endpoint implements one algorithm: corner features pooled per intersection and weighted deliberately simply. The weights, the field names, and the scoring itself may change as we learn, and because the /v3 API is a preview surface, such changes are made in place rather than under a new version.
Read a score as one lens rather than a verdict, and pin your expectations if you build on it — or compute your own index from the Label Clusters API, whose clusters carry the same intersection_id.
The AccessScore: Intersections API returns an accessibility score in the range (0, 1) for each street intersection — higher is more accessible. Curb ramps, missing curb ramps, crosswalks, and pedestrian signals are corner features, so they are pooled on the intersection they sit at, across every street meeting there, rather than credited to whichever street the labeler happened to walk. The score summarizes what was found at the corners. It does not trace any one crossing from a ramp, over a crosswalk, to the ramp on the far side, so it is not a verdict on whether a particular crossing is passable.
Intersections are derived from Project Sidewalk's own street network: every place three or more streets meet. Bridges and tunnels that only cross a street are marked grade_separated and never scored. See the AccessScore: Streets API for how a street's score combines its own segment with its end intersections.
AccessScore extends the method introduced in Li et al., A Pilot Study of Sidewalk Equity in Seattle (Urban Access 2022).
AccessScore: Intersections Preview #
Below is a live preview of intersection AccessScores from a sample region in Chicago, IL, retrieved directly from the API. Each intersection is colored from red (low accessibility) to green (high); gray intersections have no audited street yet, and hollow ones are grade-separated crossings. Hover an intersection to highlight it; click for its score and the corner features that produced it.
One method, not the method. There is no single correct way to measure whether an intersection is crossable — researchers and practitioners use many different approaches. AccessScore implements just one such algorithm, and it is experimental: the weighting is deliberately simple and subject to change. Treat these scores as one lens rather than a definitive measure, and compute your own index from the Label Clusters API if a different method suits your needs better.
Endpoint#
Returns an intersection and its AccessScore for each intersection in the queried area, optionally filtered by the Query Parameters below.
GET /v3/api/accessScoreIntersections
Examples#
/v3/api/accessScoreIntersections?filetype=geojson Get AccessScores for all intersections in GeoJSON (default)
/v3/api/accessScoreIntersections?filetype=geojson&inline=true Same, but opened in the browser
/v3/api/accessScoreIntersections?filetype=csv Get AccessScores for all intersections in CSV
/v3/api/accessScoreIntersections?regionId=8 Get scores for the intersections in a single region
How the score is computed#
Where intersections come from. Project Sidewalk's street network is fully noded: wherever two streets meet, they share an endpoint. An intersection is a group of street endpoints (merged within about a meter) where three or more distinct streets meet; its degree is that count. Two endpoints meeting is a street that was split, not an intersection, and one is a dead end.
Bridges and tunnels. The street network stores a bridge crossing a street as if the two met, so those false intersections are detected from OpenStreetMap's bridge, tunnel, and layer tags: an intersection is grade_separated when two or more of the streets passing through it sit on different layers. A street that merely ends at a bridge abutment is a real intersection. A grade-separated intersection is listed for completeness but never scored, and no cluster is attributed to it.
Which clusters score an intersection. Project Sidewalk clusters proximal labels of the same type. Each cluster of an intersection type — CurbRamp, NoCurbRamp, Crosswalk, Signal — is attributed to the nearest non-grade-separated intersection within 25 m of it, on whichever street it sits. Measured on Seattle against the city's curb ramp inventory, about 85% of these clusters attach to the intersection the city keys the ramp to and 1% to a neighbouring one (their median distance to an intersection is 9–15 m); most of the remainder sit at junctions missing from our street network, the rest are mid-block crosswalks and driveway ramps. An unattached cluster stays with its street and scores its segment instead. The along-length types (Obstacle, SurfaceProblem, NoSidewalk) always score the segment.
The score. The attributed clusters contribute exactly as they do on a street — base(type) × ratingMultiplier + Σ tagAdjustments per cluster, a Bad curb ramp counting against, tags such as APS or missing tactile warning adjusting — and the intersection's score is the logistic (sigmoid) of the summed terms. There is no length to normalize by. An intersection is scored once any street meeting it has been audited; before that its score is null, since an absence of labels means an absence of data, not a perfect corner. The weights are the ones tabulated on the AccessScore: Streets API page, read from the same scoring engine.
What is not in the score yet. degree is reported but not scored: two curb ramps at a four-way intersection read as two ramps, not as an incomplete corner set. Which side of each street a ramp serves is likewise not yet modeled.
The per-type cluster_counts and sub_scores in each response let you see exactly how an intersection's score was composed: the sub_scores sum to logit(score). With severity_counts, tag_adjustments, and the scoring configuration, you can recompute an intersection's score under your own weights exactly as for a street, with no length factor.
Quick Download #
Download intersection AccessScore data directly in your preferred format:
Note: This downloads scores for all intersections. For filtered data, use the API Query Parameters described below.
Query Parameters#
All parameters are optional.
Note: When multiple location filters are provided (bbox, regionId, and regionName), bbox takes precedence over region filters, and regionId takes precedence over regionName. Every intersection's score is computed from all of its streets, whether or not they lie inside the filter.
| Parameter | Type | Description |
|---|---|---|
bbox |
string |
Filter by bounding box, as minLongitude,minLatitude,maxLongitude,maxLatitude (e.g., -74.04,40.88,-74.00,40.91), WGS84 (EPSG:4326). Returns the intersections inside the box. If omitted, the city's default bounding box is used. |
regionId |
integer |
Return only intersections in the region with this id. Takes precedence over regionName; bbox takes precedence over both. |
regionName |
string |
Return only intersections in the region with this name. Used only when bbox and regionId are absent. |
filetype |
string |
Output format. Options: geojson (default), csv, shapefile, geopackage. |
inline |
boolean |
Whether to display the file inline rather than as an attachment. Default: false. |
Responses#
Success Response (200 OK)#
On success, the API returns 200 OK and the requested data in the specified filetype format.
GeoJSON Format (Default) #
Returns a GeoJSON FeatureCollection where each feature is an intersection (Point, WGS84 / EPSG:4326).
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-74.0243, 40.8839] },
"properties": {
"intersection_id": 412,
"region_id": 8,
"degree": 4,
"grade_separated": false,
"street_edge_ids": [766, 767, 1203, 1204],
"audit_count": 5,
"score": 0.8176,
"label_count": 11,
"cluster_counts": { "CurbRamp": 3, "NoCurbRamp": 1, "Crosswalk": 2, "Signal": 0 },
"sub_scores": { "CurbRamp": 1.875, "NoCurbRamp": -0.67, "Crosswalk": 0.5, "Signal": 0.0 },
"severity_counts": {
"CurbRamp": {"1": 2, "2": 1, "3": 0, "null": 0},
"NoCurbRamp": {"1": 0, "2": 1, "3": 0, "null": 0},
"Crosswalk": {"1": 0, "2": 2, "3": 0, "null": 0},
"Signal": {"1": 0, "2": 0, "3": 0, "null": 0}
},
"tag_adjustments": { "CurbRamp": 0.0, "NoCurbRamp": 0.0, "Crosswalk": -0.25, "Signal": 0.0 }
}
},
...
]
}
GeoJSON Field Descriptions #
| Field Path | Type | Description |
|---|---|---|
geometry.coordinates | array[number] | The intersection's position, in [longitude, latitude] (WGS84 / EPSG:4326): the centroid of the street endpoints meeting there. |
properties.intersection_id | integer | Project Sidewalk's identifier for this intersection. Stable across nightly rebuilds unless the street network itself changes. |
properties.region_id | integer | Identifier of the region most of the intersection's streets belong to; null if none of them is in a region. |
properties.degree | integer | How many streets meet here (always at least 3). |
properties.grade_separated | boolean | true for a bridge or tunnel crossing rather than a place to cross. Never scored; holds no clusters. |
properties.street_edge_ids | array[integer] | The street_edge_id of every street meeting here (see the Streets API). |
properties.audit_count | integer | Completed (high-quality) audits summed over those streets. |
properties.score | number | AccessScore in (0, 1); higher is more accessible. null when none of the intersection's streets has been audited, or it is grade-separated. |
properties.label_count | integer | Number of labels contributing to this intersection's clusters. |
properties.cluster_counts | object | Number of clusters of each intersection type attributed here, keyed by label-type name (CurbRamp, NoCurbRamp, Crosswalk, Signal only). |
properties.sub_scores | object | Contribution of each type to the pre-sigmoid total, keyed by label-type name; the values sum to logit(score). |
properties.severity_counts | object | Number of clusters of each type in each rating bucket ("1", "2", "3", or "null" for unrated clusters). The buckets of a type sum to its cluster_counts entry. |
properties.tag_adjustments | object | The summed adjustment of the active tags of each type: the part of sub_scores that no weight scales. |
CSV Format #
If filetype=csv, the first row is the header. The per-type objects are flattened into one column per intersection type, each named for its path through the JSON: cluster_counts.<type>, sub_scores.<type>, severity_counts.<type>.1 through .3 plus .null, and tag_adjustments.<type>; street_edge_ids is a JSON list, and the geometry becomes lat, lng.
intersection_id,region_id,degree,grade_separated,street_edge_ids,audit_count,score,label_count,cluster_counts.CurbRamp,cluster_counts.NoCurbRamp,cluster_counts.Crosswalk,cluster_counts.Signal,sub_scores.CurbRamp,…,sub_scores.Signal,severity_counts.CurbRamp.1,…,severity_counts.Signal.null,tag_adjustments.CurbRamp,…,tag_adjustments.Signal,lat,lng
412,8,4,false,"[766,767,1203,1204]",5,0.8176,11,3,1,2,0,1.875,…,0.0,2,…,0,0.0,…,0.0,40.8839,-74.0243
...
The full header, in order: the eight leading columns, then cluster_counts.<type> for each of the 4 intersection types, sub_scores.<type> for each, the four severity_counts.<type>.* buckets for each, tag_adjustments.<type> for each, then lat and lng. Types are always in the order CurbRamp, NoCurbRamp, Crosswalk, Signal.
Shapefile Format #
If filetype=shapefile, the response is a ZIP archive of Shapefile components (.shp, .shx, .dbf, .prj). Because the DBF format truncates column names at 10 characters, the columns use short codes: intersecId, gradeSep, streetIds (comma-separated), and per type the same nCRamp / sCRamp / n1CRamp…n0CRamp / tCRamp codes as the street shapefile; the GeoJSON, CSV, and GeoPackage formats keep the full names.
GeoPackage Format #
If filetype=geopackage, the response is a GeoPackage (.gpkg) file with full geometry and attributes (full snake_case column names).
Error Responses#
400 Bad Request: Invalid parameter values (e.g., malformed bounding box, non-positive or unknown region id).500 Internal Server Error: An unexpected error occurred on the server.
Error Response Body #
All errors are returned as RFC 7807 “problem details” with the application/problem+json content type and the following structure:
{
"type": "about:blank", // RFC 7807 problem-type URI ("about:blank" means no type beyond the status)
"title": "Invalid Parameter", // Short, human-readable summary of the problem type (stable for a given code)
"status": 400, // HTTP status code (also repeated in the body)
"detail": "Invalid value for the bbox parameter. Expected format: minLng,minLat,maxLng,maxLat.", // This occurrence
"code": "INVALID_PARAMETER", // Stable, machine-readable error code you can branch on
"parameter": "bbox" // Extension member: the specific parameter at fault (omitted when not applicable)
}
Best Practices#
- Treat the score as relative, not absolute: the weights are experimental. Use scores to compare intersections, not as a calibrated index.
- Mind
audit_count: anullscore means no street at the intersection has been audited — absence of data, not a poor corner — and a low count means only some approaches have been seen. - Skip
grade_separatedintersections when counting crossings: they are listed so the network is complete, not because anyone crosses there. - Join to streets:
street_edge_idshere andstart_intersection_id/end_intersection_idon the Streets API are the same identifiers, so the two can be joined into a graph.
Contribute#
Project Sidewalk is an open-source project created by the Makeability Lab and hosted on GitHub. We welcome your contributions! If you found a bug or have a feature request, please open an issue on GitHub.
You can also email us at sidewalk@cs.uw.edu
Project Sidewalk in Your City!#
If you are interested in bringing Project Sidewalk to your city, please read our Wiki page.