openwalkscore
Open, transparent, API-key-free walkability scores (0-100) for any point or area on Earth, computed from OpenStreetMap.
openwalkscore measures how many everyday destinations (groceries, cafes, schools, parks,
clinics, transit stops and more) you can reach on foot, and how walkable the street network
around you is. Every number in the method is published below and lives in an editable
profile, so a score can always be reproduced, audited and adapted.
It is written for urban data scientists, real-estate and proptech developers, planners and students who need a walkability indicator they can explain, and that works for any city.
Disclaimer. openwalkscore is an independent open-source project. It is not affiliated with, endorsed by or connected to Walk Score® or Redfin. Walk Score® is a registered trademark of its owner. openwalkscore uses its own method and its own data (OpenStreetMap); its scores are not comparable to Walk Score® values and must not be presented as such.
Contents
- Install
- Quick start
- Methodology
- Profiles
- Python API
- Command line
- Data sources and offline use
- HTML maps
- Limitations
- Citing
- License and attribution
Install
pip install openwalkscore
# optional: run shortest paths with networkx instead of the built-in Dijkstra
pip install "openwalkscore[networkx]"
Python 3.9 or newer. The only runtime dependency is certifi (plus tomli on Python < 3.11).
No API key, no account, no osmnx/geopandas stack required.
Quick start
from openwalkscore import score_point
r = score_point(48.8530, 2.3499) # Ile de la Cite, Paris
print(r.score, r.rating) # e.g. 100 Excellent
print(r.subscores) # {'grocery': 100.0, 'restaurants_cafes': 100.0, ...}
print(r.explain()) # plain-text, line-by-line justification
for hit in r.nearest_amenities(limit=5):
print(f"{hit.distance_m:5.0f} m {hit.category:<18} {hit.name}")
The first call downloads amenities and the walking network around the point from the public Overpass API (this can take up to a minute in a dense city centre) and caches the response on disk; repeated calls are instant.
openwalkscore score --lat 48.8530 --lon 2.3499
openwalkscore explain --lat 48.8530 --lon 2.3499
openwalkscore grid --bbox 2.340,48.850,2.360,48.860 --cell 200 -o paris.geojson --html paris.html
Methodology
For a point p and a profile with categories c (weight W_c):
1. Amenity categories
Each OpenStreetMap feature with a location (node, or way/relation centroid) is assigned to the
first category whose tag rules match (shop=* comes after the grocery and healthcare
rules, so a supermarket counts as grocery, not shopping). Default profile:
| Category | Weight | Counted (n) | Ratio (r) | Tags (abridged) |
|---|---|---|---|---|
| Grocery | 3 | 2 | 0.4 | shop=supermarket/convenience/greengrocer/bakery/butcher/deli, amenity=marketplace |
| Restaurants & cafes | 3 | 10 | 0.8 | amenity=restaurant/cafe/fast_food/pub/bar/food_court/ice_cream |
| Healthcare | 1 | 2 | 0.5 | amenity=pharmacy/clinic/doctors/hospital/dentist, shop=chemist, healthcare=* |
| Shopping | 2 | 5 | 0.7 | shop=* (except vacant, car, car_parts, fuel) |
| Schools | 1 | 1 | - | amenity=school/kindergarten/childcare/college/university |
| Parks & play | 1 | 2 | 0.5 | leisure=park/playground/garden/recreation_ground/nature_reserve (not access=private) |
| Banks & post | 1 | 1 | - | amenity=bank/atm/bureau_de_change/post_office |
| Culture & community | 1 | 2 | 0.5 | amenity=library/theatre/cinema/arts_centre/community_centre, tourism=museum/gallery |
| Transit stops | 2 | 3 | 0.5 | highway=bus_stop, public_transport=platform, railway=station/halt/tram_stop/subway_entrance, amenity=ferry_terminal |
Run openwalkscore explain (no coordinates) to print the complete profile as JSON.
2. Walking distance
- The walking network is built from OSM ways with a walkable
highway=*value (footway, path, pedestrian, steps, living_street, residential, service, tertiary ... primary, track, cycleway, ...). Motorways and trunk roads are excluded unlessfoot=yes;foot=no,access=privateandfoot=use_sidepathare respected. - Every pair of consecutive way nodes is an undirected edge weighted by its great-circle length.
- The scored point and every amenity are snapped to the nearest edge (not node), and the perpendicular access legs are added to the route.
- Shortest paths come from a pure-Python multi-source Dijkstra that starts part-way along
the origin edge and stops at the decay cut-off (1.6 km).
engine="networkx"gives identical results with networkx. - Fallback: amenities more than
max_snap_m(250 m) from the network, or every amenity when the point itself has no network nearby, use straight-line distance x detour factor 1.3. The result reportsdistance_mode(network,straight-lineormixed) per amenity and overall. - An amenity that is close but not connected to the point's network (for example across a river with no bridge in the data) gets no credit.
3. Distance decay
Credit for an amenity at walking distance d:
f(d) = 1 if d <= 400 m
f(d) = 0.5 * (1 + cos(pi * t)) if 400 m < d < 1600 m, t = (d - 400) / 1200
f(d) = 0 if d >= 1600 m
The cosine taper is smooth at both ends: an amenity 700 m away earns 0.85, 1 km away 0.50,
1.3 km away 0.15. A linear ramp is available (decay.shape = "linear").
4. Diminishing returns
Within a category the amenities are ranked by credit and the best n are kept. The k-th one is weighted by
s_k = r^(k-1) / (1 + r + ... + r^(n-1))
so the shares sum to 1 and each extra cafe adds less than the previous one. The category
subscore is S_c = sum_k s_k * f(d_k), between 0 and 1. Example (restaurants & cafes,
n = 10, r = 0.8): one cafe next door is worth 22 % of the category, five cafes 75 %, ten
cafes 100 %.
5. Access score
A = 100 * sum_c W_c * S_c / sum_c W_c
6. Network penalties
Measured within analysis_radius_m (800 m) of the point:
| Penalty | Metric | No penalty at | Full penalty at | Max |
|---|---|---|---|---|
| Intersection density | nodes where >= 3 street edges meet, per km² | >= 90 /km² | <= 30 /km² | 10 % |
| Block length | mean length of street segments between intersections / dead ends | <= 120 m | >= 240 m | 10 % |
| Slope (only with a DEM) | length-weighted mean absolute grade of those segments | <= 3 % | >= 15 % | 10 % |
Each penalty is linear between its two thresholds. They combine multiplicatively,
P = 1 - prod_i (1 - P_i), capped at max_total_penalty (30 %). Penalties are skipped
(and the explanation says so) when the point has no network nearby.
7. Final score
score = round(A * (1 - P)) # integer 0..100
| Score | Rating |
|---|---|
| 90-100 | Excellent |
| 70-89 | High |
| 50-69 | Moderate |
| 25-49 | Low |
| 0-24 | Very low |
Profiles
Built-in profiles: default, family (schools, parks and healthcare weighted up),
senior (300 m full credit, 1 km cut-off, healthcare and transit weighted up, stronger slope
penalty) and transit (transit stops dominate).
A custom profile overrides a base profile key by key, from a dict or a TOML file:
# nightlife.toml
[profile]
name = "nightlife"
base = "default"
[profile.decay]
full_m = 300
zero_m = 1200
shape = "linear"
[profile.categories.restaurants_cafes]
weight = 6
max_count = 12
[profile.categories.nightclubs] # a new category
weight = 2
tags = ["amenity=nightclub", "amenity=music_venue"]
max_count = 3
ratio = 0.6
[profile.penalties.slope]
max_penalty = 0.15
from openwalkscore import Profile, score_point
p = Profile.from_toml("nightlife.toml")
p = Profile.from_dict({"categories": {"shopping": {"weight": 0}}}) # switch a category off
score_point(52.52, 13.405, profile=p)
score_point(52.52, 13.405, profile="nightlife.toml") # a path works too
Unknown keys raise ValueError, so a typo never silently changes a score. Set
replace_categories = true to start from an empty category list.
Python API
| Function / class | Purpose |
|---|---|
score_point(lat, lon, profile="default", *, data=None, dem=None, engine="python", client=None, use_cache=True, name="") |
Score one point; returns WalkScoreResult. |
score_grid(bbox, cell_m=250, profile="default", *, data=None, ...) |
Score square cells over bbox = (west, south, east, north); returns a GeoJSON FeatureCollection with score, rating, sub_<category> per cell. |
compare(points, profile="default", *, data=None, ...) |
Score (lat, lon), (name, lat, lon) or {"name", "lat", "lon"} items; close points share one download. |
WalkScorer(data, profile, dem=None, engine="python") |
Build the network once, call .score(lat, lon) / .grid(bbox, cell_m) many times. |
WalkScoreResult |
score, rating, access_score, total_penalty, subscores, categories (with counted AmenityHits), penalties, metrics, distance_mode, explain(), nearest_amenities(), to_dict(), to_geojson_feature(). |
Profile, get_profile, list_profiles |
Profiles and their configuration. |
OverpassClient(endpoints, timeout_s, cache_dir, cache_ttl_s, min_interval_s, retries) |
Download control. |
load_osm(path_or_obj) |
Read .osm, .geojson, Overpass .json, or a dict. |
WalkNetwork, dijkstra |
The graph and the shortest-path routine, usable on their own. |
DEM.from_ascii_grid(path), DEM.from_function(fn) |
Elevation for the slope penalty. |
render_html(obj), write_html(obj, path) |
Leaflet map of a result, a list of results or a grid. |
from openwalkscore import WalkScorer, load_osm
scorer = WalkScorer(load_osm("district.osm"), profile="family")
results = [scorer.score(lat, lon) for lat, lon in listings] # fast: graph built once
Using a DEM:
score_point(41.03, 28.98, dem="istanbul_wgs84.asc") # ESRI ASCII grid in degrees
import rasterio # or any sampler you like
src = rasterio.open("dem_wgs84.tif"); band = src.read(1)
def elevation(lat, lon):
row, col = src.index(lon, lat)
return float(band[row, col])
score_point(41.03, 28.98, dem=elevation)
Command line
openwalkscore score --lat LAT --lon LON [--profile P] [--json] [--html map.html]
openwalkscore explain [--lat LAT --lon LON] [--profile P]
openwalkscore grid --bbox WEST,SOUTH,EAST,NORTH --cell 250 -o grid.geojson [--html map.html]
openwalkscore compare --point "Home,41.03,28.98" --point "Office,41.04,29.00" [--json]
openwalkscore profiles
Common options: --input FILE (offline .osm/.geojson/.json), --dem FILE.asc,
--engine python|networkx, --timeout SECONDS, --endpoint URL (repeatable),
--no-cache, --cache-dir DIR. Exit codes: 0 ok, 2 invalid input, 3 download failed.
explain without coordinates prints the method summary and the full profile.
Data sources and offline use
Overpass API (default). OverpassClient sends one query per area (amenities with
out center, plus the walkable ways and their nodes) to public Overpass instances, trying
overpass-api.de, overpass.kumi.systems and overpass.private.coffee in turn. It:
- sets the generic User-Agent
openwalkscore/<version>; - applies a network timeout (default 60 s) and retries HTTP 429/502/503/504 with back-off before failing over to the next mirror;
- waits at least 1 s between requests from the same process;
- caches responses on disk for 7 days (
%LOCALAPPDATA%\openwalkscore\cache,~/.cache/openwalkscore, orOPENWALKSCORE_CACHE_DIR); - refuses areas with a diagonal above 25 km. For a whole city, download an extract once
(e.g. from Geofabrik, then clip with
osmium) and pass it withdata=/--input.
Please follow the Overpass usage policy: keep areas small and avoid parallel bulk jobs.
Offline input. data= (or --input) accepts:
.osm/.xml- OSM XML (JOSM, OSM API,osmium cat -f osm);- Overpass JSON (
{"elements": [...]}) saved from overpass-turbo or the API; - GeoJSON - points become amenities,
LineStrings with ahighwayproperty become the network (touching vertices are joined), polygons become amenities at their centroid. Tags are read frompropertiesorproperties.tags.
HTML maps
from openwalkscore import score_point, score_grid, write_html
write_html(score_point(40.4168, -3.7038), "madrid.html")
write_html(score_grid((-3.71, 40.41, -3.69, 40.425), 200), "madrid_grid.html")
The page loads Leaflet 1.9.4 from unpkg with Subresource Integrity hashes and shows
OpenStreetMap tiles with the required attribution. Data is embedded as JSON with <, >
and & escaped, and all OSM-supplied text (names, tags) is inserted with textContent, so
feature names cannot inject markup or script.
Limitations
- OSM completeness varies. Missing shops or footpaths lower the score; a score is a statement about the map as much as about the place. Compare places within regions of similar mapping quality.
- Sidewalks mapped as separate footways add intersections and short segments; the
density and block-length penalties are therefore lenient in such areas by design (they
rarely trigger where sidewalks are mapped). Penalties near the edge of a local extract
can be overstated if the extract does not cover
analysis_radius_m. - No crossing safety, traffic speed, sidewalk width, lighting, opening hours, shade, crime or amenity quality is modelled.
- The network is treated as undirected and of uniform walking speed; steps and steep grades only enter through the optional slope penalty.
- Amenity counts are capped per category; a mall with 200 shops counts as at most 5 shops in the default profile.
Development
git clone https://github.com/YusufEminoglu/openwalkscore_sdk
cd openwalkscore_sdk
pip install -e ".[dev]"
python -m pytest # no network needed: tests use hand-made OSM fixtures
python -m ruff check . && python -m ruff format --check src tests && python -m mypy src
Citing
If you use openwalkscore in research, please cite it via CITATION.cff:
Eminoğlu, Y. (2026). openwalkscore: open, transparent walkability scores from OpenStreetMap (version 0.1.0) [Software]. https://github.com/YusufEminoglu/openwalkscore_sdk
License and attribution
Code: MIT, © 2026 Yusuf Eminoğlu. See LICENSE.
Map data: © OpenStreetMap contributors, available under the Open Database License (ODbL). When you publish scores or maps derived from OSM data, credit OpenStreetMap accordingly.
Walk Score® is a registered trademark of its owner; this project is not affiliated with it.
Metadata
Release files for openwalkscore 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| openwalkscore-0.1.0.tar.gz | 69.0 kB | With provenance | Details for openwalkscore-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| openwalkscore-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for openwalkscore-0.1.0-py3-none-any.whl |
Total release size: 117.9 kB
Release files / openwalkscore-0.1.0.tar.gz
| Download URL | openwalkscore-0.1.0.tar.gz |
|---|---|
| Size | 69.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eedd28ccc348479d5beb75b8ca9c5198d7fdd702b289dffe415212419d5f3827
|
|
BLAKE2b-256 checksum How to use checksums |
8b9fd121325389c478648f5d963b38fb5f86fb6283458acc1e5d5c394f77236e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / openwalkscore-0.1.0-py3-none-any.whl
| Download URL | openwalkscore-0.1.0-py3-none-any.whl |
|---|---|
| Size | 49.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dcd15d19b97612cd549a47e95a85bb97cea8e65bee9cbd0a3c982c59da776619
|
|
BLAKE2b-256 checksum How to use checksums |
704f248e1791332b2ac4822b686ce098b8e1fc6b3b1d7e374367f272480398c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for openwalkscore 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for openwalkscore-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.