AccessScore: Streets API #
The AccessScore: Streets API returns an accessibility score in the range (0, 1) for each street — higher is more accessible. A street's score combines three things: its own segment, scored from the obstacles, surface problems, and missing sidewalks along it, and the intersections at its two ends, scored from the curb ramps, crosswalks, and signals pooled at each corner (see the AccessScore: Intersections API). Each piece weights every clustered feature by its type, its severity/quality rating, and its tags, then squashes the total with a logistic function.
AccessScore extends the method introduced in Li et al., A Pilot Study of Sidewalk Equity in Seattle (Urban Access 2022). It is experimental: the weights are deliberately simple and subject to change. For full control you can compute your own index from the Label Clusters API.
AccessScore: Streets Preview #
Below is a live preview of street AccessScores from a sample region in Washington, DC, retrieved directly from the API. Streets are colored from red (low accessibility) to green (high); gray streets have not yet been audited. Hover or click a street to see its score and the accessibility features that produced it.
One method, not the method. There is no single correct way to measure street-level accessibility—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 a street segment and its AccessScore for each street in the queried area, optionally filtered by the Query Parameters below.
GET /v3/api/accessScoreStreets
Examples#
/v3/api/accessScoreStreets?filetype=geojson Get AccessScores for all streets in GeoJSON (default)
/v3/api/accessScoreStreets?filetype=geojson&inline=true Same, but opened in the browser
/v3/api/accessScoreStreets?filetype=csv Get AccessScores for all streets in CSV
/v3/api/accessScoreStreets?regionId=8 Get scores for streets in a single region
How the score is computed#
Project Sidewalk clusters proximal labels of the same type, and scores two kinds of unit from them: a street's segment and an intersection. Four of the seven scored label types are corner features — CurbRamp, NoCurbRamp, Crosswalk, Signal — so each of their clusters is attributed to the nearest intersection within 25 m (on whichever street it sits) and scores that intersection, pooled with the corner features of every other street meeting there. The rest of the street's clusters score its segment: the along-length types (Obstacle, SurfaceProblem, NoSidewalk), plus any corner-type cluster too far from an intersection to belong to one (a mid-block crosswalk, a driveway ramp), which counts as an ordinary point feature.
A street's headline score is the plain mean of its segment_score and the scores of its start and end intersections, over whichever of the three exist — a trip along a street includes getting on and off it. An end with no intersection (a dead end, a street split at a region boundary) or a grade-separated one contributes nothing, and an unaudited street between two scored crossings still gets a headline from them. All three components are reported, so you can recombine them your own way.
Scoring a segment. Each scored label type contributes a signed term; the segment's score is the logistic (sigmoid) of the sum of those terms, mapped to (0, 1). An audited street with nothing on it scores the neutral 0.5.
Most types are scored per cluster. Each cluster contributes base(type) × ratingMultiplier + Σ tagAdjustments, and the type's term is the sum over its clusters on the street:
- Positive, quality-rated (CurbRamp, Crosswalk): rated Good / Okay / Bad → multiplier
+1.0 / +0.5 / −1.0. A Bad feature therefore lowers the score. - Negative, severity-rated (NoCurbRamp, Obstacle, SurfaceProblem): rated Low / Medium / High → magnitude
0.33 / 0.67 / 1.0of the (negative) base. - Presence-only (Signal): no rating; each cluster adds the base weight as-is.
- Tags adjust a cluster's weight when present on at least half of its labels — e.g. a Signal tagged hard to reach buttons counts negatively, while APS (accessible signal) counts positively.
- Length normalization (Obstacle, SurfaceProblem): these describe problems along a street, so their terms are scaled to a density per 100 m —
term × 100 / max(length_meters, 25). Three obstacles on a 300 m street score as one on a 100 m street; a long street is not penalized for having more room for problems. The 25 m floor keeps a stub of a street from multiplying one problem without bound, so the factor never exceeds 4.
NoSidewalk is scored once per street, as a street condition, rather than per cluster. A missing sidewalk is a stretch, not a point: labelers pin it repeatedly along the street and it is clustered at 10 m, so a per-cluster weight would measure how many pins were dropped instead of whether the street has a sidewalk. All of a street's NoSidewalk clusters pool into a single term, which is what score_no_sidewalk reports:
score_no_sidewalk = base × min(1, n / 3) + Σ tagAdjustments
nis the street's NoSidewalk cluster count (n_no_sidewalk). The extent factormin(1, n / 3)saturates at 3 clusters: one stray pin counts1 / 3of the base weight, and 3 or more count the full weight however many more there are.- Known limitation. A cluster count is a proxy for coverage, and NoSidewalk clusters at 10 m, so how readily a street reaches 3 clusters depends on how long it is. A very short street without a sidewalk can be structurally unable to saturate, and so scores somewhat higher than a long street in the same condition. Pooling removes this effect above saturation but not below it.
- Street-wide tags are judged over the street's pooled NoSidewalk labels: a tag adjusts the term when at least half of those labels carry it. street has no sidewalks and street has a sidewalk (the other side of the street has one) are mutually exclusive, so at most one is active unless the labels split exactly in half, in which case they cancel.
- ends abruptly marks the point where a sidewalk stops, so it is active when any single cluster carries it on at least half of that cluster's labels. It aggravates the score: a pedestrian on the sidewalk is stranded in the roadway where it ends.
- Worked example (current weights, before the street's other features): a street with eight NoSidewalk clusters and no tags scores
sigmoid(-2.00) ≈ 0.12; tagged street has no sidewalks it drops tosigmoid(-3.00) ≈ 0.05; tagged street has a sidewalk it rises tosigmoid(-1.00) ≈ 0.27. A single NoSidewalk cluster with no tags issigmoid(-0.67) ≈ 0.34.
The base weights and tag adjustments below are read directly from the scoring engine, so they always match what the API computes:
| Label type | Base weight | Rating | Tag adjustments |
|---|---|---|---|
CurbRamp |
+0.75 |
Quality (Good / Okay / Bad) |
missing tactile warning
-0.25narrow
-0.25points into traffic
-0.25steep
-0.25 |
NoCurbRamp |
-1.00 |
Severity (Low / Medium / High) |
alternate route present
+0.25no alternate route
-0.50 |
Obstacle |
-1.00 |
Severity (Low / Medium / High) | — |
SurfaceProblem |
-1.00 |
Severity (Low / Medium / High) | — |
Crosswalk |
+0.75 |
Quality (Good / Okay / Bad) |
level with sidewalk
+0.25no pedestrian priority
-0.25paint fading
-0.25 |
Signal |
+0.50 |
Presence only, per cluster |
APS
+0.25button waist height
+0.15hard to reach buttons
-0.25 |
NoSidewalk |
-2.00 |
Street condition: one pooled term per street |
covered walkway
+0.50ends abruptly
-1.00gravel/dirt road
-0.25pedestrian lane marking
+0.50shared pedestrian/car space
+0.25street has a sidewalk
+1.00street has no sidewalks
-1.00 |
The per-type cluster_counts and sub_scores in each response let you see exactly how a street's segment was composed: the sub_scores sum to logit(segment_score). A corner type appearing there is a mid-block cluster; the ones at the corners are on the intersections. Other label types (Occlusion, Other) do not affect the score.
Note: Region scores (see the AccessScore: Regions API) are the length-weighted mean of their streets' headline scores, alongside a plain mean of their intersections.
Recomputing the score under your own weights#
Every street carries the inputs to its score, so you can re-weight it yourself. GET /v3/api/accessScoreConfig returns the engine's configuration — the scored types in order, each type's base weight and scoring mode, the rating multipliers per bucket, the tag adjustments, the street-condition saturation count, and a few named weight presets — as JSON:
GET /v3/api/accessScoreConfig
Then, per street, each type's segment term is rebuilt from severity_counts, tag_adjustments, and length_meters:
- Per-cluster types (
positive_quality,negative_severity,presence_only):term = weight × Σbucket severity_counts[type][bucket] × multiplier(bucket) + tag_adjustments[type], where the multiplier comes fromquality_multiplierorseverity_multiplierby scoring mode, and is1forpresence_only. - Street-condition types (
street_condition):term = weight × min(1, n / street_condition_saturation_count) + tag_adjustments[type], withnthe type's total cluster count. - Length-normalized types (
length_normalizedis true): multiply the whole term bylength_normalization.per_meters / max(length_meters, length_normalization.min_length_meters). segment_score = sigmoid(Σtype term). With the engine's ownbase_weightfor everyweight, the terms equalsub_scoresand the result equalssegment_score.- An intersection is rebuilt the same way from its own
severity_countsandtag_adjustments, with no length factor; the street's headlinescoreis then the mean ofsegment_scoreand the scores ofstart_intersection_idandend_intersection_id, over those that are notnull.
The response also carries clusters_updated_at: when the nightly clustering run that every score is computed from last finished (ISO 8601, or null before the first run). Labels added after that moment are not in any cluster yet and cannot have changed a score until the next run.
The presets object holds named weight vectors (magnitudes; a type's sign is fixed by its base weight): default is the engine's own weights, barriers weighs problem types half again as much, infrastructure does the same for feature types, and missing_ramps doubles missing curb ramps. They are named by what they change, not by whom they are for — a weighting that claims to represent a disability community would need that community's involvement and the literature behind it — and every value is published, so a lens hides nothing.
Quick Download #
Download street AccessScore data directly in your preferred format:
Note: This downloads scores for all streets. 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. A region filter is resolved to the region's bounding box, and the streets are then trimmed back to that region.
| 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). If omitted, the city's default bounding box is used. |
regionId |
integer |
Score only streets within the region with this id. Takes precedence over regionName; bbox takes precedence over both. |
regionName |
string |
Score only streets within 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 a street segment (LineString, WGS84 / EPSG:4326).
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-74.0243, 40.8839], [-74.0245, 40.8836]]
},
"properties": {
"street_edge_id": 766,
"osm_way_id": 11584845,
"region_id": 8,
"score": 0.6849,
"segment_score": 0.3846,
"start_intersection_id": 412,
"end_intersection_id": 415,
"start_intersection_score": 0.8176,
"end_intersection_score": 0.8525,
"audit_count": 2,
"length_meters": 142.7,
"label_count": 4,
"cluster_counts": {
"CurbRamp": 0, "NoCurbRamp": 0, "Obstacle": 0, "SurfaceProblem": 1,
"Crosswalk": 1, "Signal": 0, "NoSidewalk": 0
},
"sub_scores": {
"CurbRamp": 0.0, "NoCurbRamp": 0.0, "Obstacle": 0.0, "SurfaceProblem": -0.4695,
"Crosswalk": 0.0, "Signal": 0.0, "NoSidewalk": 0.0
},
"severity_counts": {
"CurbRamp": {"1": 0, "2": 0, "3": 0, "null": 0},
"NoCurbRamp": {"1": 0, "2": 0, "3": 0, "null": 0},
"Obstacle": {"1": 0, "2": 0, "3": 0, "null": 0},
"SurfaceProblem": {"1": 0, "2": 1, "3": 0, "null": 0},
"Crosswalk": {"1": 0, "2": 0, "3": 1, "null": 0},
"Signal": {"1": 0, "2": 0, "3": 0, "null": 0},
"NoSidewalk": {"1": 0, "2": 0, "3": 0, "null": 0}
},
"tag_adjustments": {
"CurbRamp": 0.0, "NoCurbRamp": 0.0, "Obstacle": 0.0, "SurfaceProblem": 0.0,
"Crosswalk": 0.0, "Signal": 0.0, "NoSidewalk": 0.0
}
}
},
...
]
}
GeoJSON Field Descriptions #
| Field Path | Type | Description |
|---|---|---|
geometry.coordinates | array[number] | Coordinate pairs forming the street's path, in [longitude, latitude] (WGS84 / EPSG:4326). |
properties.street_edge_id | integer | Project Sidewalk's unique identifier for this street segment. |
properties.osm_way_id | integer | OpenStreetMap Way ID for the street, if available. |
properties.region_id | integer | Identifier for the region the street belongs to. |
properties.score | number | Headline AccessScore in (0, 1); higher is more accessible. The mean of segment_score, start_intersection_score, and end_intersection_score over those that are not null; null when all three are. |
properties.segment_score | number | The street's own segment score in (0, 1), from the clusters in cluster_counts. null when the street has not been audited. |
properties.start_intersection_id | integer | The intersection at the start of the street's geometry (see the Intersections API); null when that end is a dead end or a mere way split. |
properties.end_intersection_id | integer | The intersection at the end of the street's geometry; null likewise. |
properties.start_intersection_score | number | That intersection's score; null when there is none, it is unscored, or it is grade-separated. |
properties.end_intersection_score | number | That intersection's score; null likewise. |
properties.audit_count | integer | Number of completed (high-quality) audits of this street. |
properties.length_meters | number | Street length in meters (geodesic). Length-weights region scores and normalizes the along-length terms. |
properties.label_count | integer | Number of labels contributing to this street's segment clusters. |
properties.cluster_counts | object | Number of clusters of each label type scoring the segment, keyed by label-type name: the along-length types, plus any corner-type cluster not attributed to an intersection. |
properties.sub_scores | object | Contribution of each label type to the segment's pre-sigmoid total, keyed by label-type name; the values sum to logit(segment_score), so they explain how it was composed. Per-cluster types report the sum over their clusters, length-normalized where the type is; NoSidewalk reports the street's single pooled term (see How the score is computed), which does not scale with cluster_counts.NoSidewalk. |
properties.severity_counts | object | Number of segment clusters of each label type in each rating bucket ("1", "2", "3", or "null" for unrated clusters), keyed by label-type name. The buckets of a type sum to its cluster_counts entry. |
properties.tag_adjustments | object | The summed adjustment of the active tags of each label type, keyed by label-type name: the part of sub_scores that no weight scales, before length normalization. With severity_counts, length_meters, and the scoring configuration, enough to recompute the segment score under your own weights. |
CSV Format #
If filetype=csv, the first row is the header. The per-type objects are flattened into one column per label 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>; the geometry is simplified to start/end points.
street_edge_id,osm_way_id,region_id,score,segment_score,start_intersection_id,end_intersection_id,start_intersection_score,end_intersection_score,audit_count,length_meters,label_count,cluster_counts.CurbRamp,…,cluster_counts.NoSidewalk,sub_scores.CurbRamp,…,sub_scores.NoSidewalk,severity_counts.CurbRamp.1,severity_counts.CurbRamp.2,severity_counts.CurbRamp.3,severity_counts.CurbRamp.null,…,severity_counts.NoSidewalk.null,tag_adjustments.CurbRamp,…,tag_adjustments.NoSidewalk,start_point,end_point
766,11584845,8,0.6849,0.3846,412,415,0.8176,0.8525,2,142.7,4,0,…,0,0.0,…,0.0,0,0,0,0,…,0,0.0,…,0.0,"-74.0243,40.8839","-74.0245,40.8836"
...
The full header, in order: the twelve leading columns, then cluster_counts.<type> for each of the 7 scored types, sub_scores.<type> for each, the four severity_counts.<type>.* buckets for each, tag_adjustments.<type> for each, then start_point and end_point. Types are always in the order CurbRamp, NoCurbRamp, Obstacle, SurfaceProblem, Crosswalk, Signal, NoSidewalk.
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: segScore, sIntId / eIntId, and sIntScore / eIntScore for the headline's components, and per type cluster count nCRamp, sub-score sCRamp, cluster count per rating bucket n1CRamp, n2CRamp, n3CRamp and n0CRamp (unrated), and tag adjustment tCRamp; 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 streets, not as a calibrated index.
- Read the breakdown:
segment_scoreand the two intersection scores show which part of a trip along the street is the problem, andcluster_countsandsub_scoresexplain the segment. - Mind audit coverage: a
nullsegment_scoremeans the street has not been audited — absence of data, not poor accessibility. Its headline may still carry its crossings' scores. - Roll up to neighborhoods: use the AccessScore: Regions API for length-weighted, region-level scores.
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.