openwalkscore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. openwalkscore-0.1.0/CHANGELOG.md +33 -0
  2. openwalkscore-0.1.0/CITATION.cff +20 -0
  3. openwalkscore-0.1.0/LICENSE +21 -0
  4. openwalkscore-0.1.0/MANIFEST.in +10 -0
  5. openwalkscore-0.1.0/PKG-INFO +401 -0
  6. openwalkscore-0.1.0/README.md +358 -0
  7. openwalkscore-0.1.0/examples/nightlife_profile.toml +19 -0
  8. openwalkscore-0.1.0/examples/quickstart.py +35 -0
  9. openwalkscore-0.1.0/pyproject.toml +133 -0
  10. openwalkscore-0.1.0/setup.cfg +4 -0
  11. openwalkscore-0.1.0/src/openwalkscore/__init__.py +82 -0
  12. openwalkscore-0.1.0/src/openwalkscore/_version.py +1 -0
  13. openwalkscore-0.1.0/src/openwalkscore/cli.py +258 -0
  14. openwalkscore-0.1.0/src/openwalkscore/dem.py +120 -0
  15. openwalkscore-0.1.0/src/openwalkscore/geo.py +106 -0
  16. openwalkscore-0.1.0/src/openwalkscore/htmlmap.py +196 -0
  17. openwalkscore-0.1.0/src/openwalkscore/network.py +405 -0
  18. openwalkscore-0.1.0/src/openwalkscore/osm.py +381 -0
  19. openwalkscore-0.1.0/src/openwalkscore/overpass.py +278 -0
  20. openwalkscore-0.1.0/src/openwalkscore/profiles.py +568 -0
  21. openwalkscore-0.1.0/src/openwalkscore/py.typed +0 -0
  22. openwalkscore-0.1.0/src/openwalkscore/scoring.py +708 -0
  23. openwalkscore-0.1.0/src/openwalkscore.egg-info/PKG-INFO +401 -0
  24. openwalkscore-0.1.0/src/openwalkscore.egg-info/SOURCES.txt +37 -0
  25. openwalkscore-0.1.0/src/openwalkscore.egg-info/dependency_links.txt +1 -0
  26. openwalkscore-0.1.0/src/openwalkscore.egg-info/entry_points.txt +2 -0
  27. openwalkscore-0.1.0/src/openwalkscore.egg-info/requires.txt +14 -0
  28. openwalkscore-0.1.0/src/openwalkscore.egg-info/top_level.txt +1 -0
  29. openwalkscore-0.1.0/tests/conftest.py +164 -0
  30. openwalkscore-0.1.0/tests/fixtures/custom_profile.toml +19 -0
  31. openwalkscore-0.1.0/tests/fixtures/slope.asc +9 -0
  32. openwalkscore-0.1.0/tests/fixtures/small.geojson +24 -0
  33. openwalkscore-0.1.0/tests/fixtures/small.osm +57 -0
  34. openwalkscore-0.1.0/tests/test_cli_html.py +233 -0
  35. openwalkscore-0.1.0/tests/test_geo_osm.py +200 -0
  36. openwalkscore-0.1.0/tests/test_network.py +187 -0
  37. openwalkscore-0.1.0/tests/test_overpass.py +191 -0
  38. openwalkscore-0.1.0/tests/test_profiles.py +168 -0
  39. openwalkscore-0.1.0/tests/test_scoring.py +325 -0
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
5
+ uses [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-08
10
+ ### Added
11
+ - First public release.
12
+ - **Scoring method**: nine weighted amenity categories (grocery, restaurants & cafes,
13
+ shopping, schools, parks & play, healthcare, banks & post, culture & community, transit
14
+ stops) with cosine distance decay (full credit up to 400 m, zero at 1.6 km) and
15
+ diminishing returns per category; penalties for low intersection density, long blocks
16
+ and, with a DEM, steep grades.
17
+ - **Network distances** on the OpenStreetMap pedestrian graph with a pure-Python
18
+ multi-source Dijkstra; points and amenities are snapped to the nearest street *edge*.
19
+ Straight-line distance x detour factor (1.3) is the fallback without a network.
20
+ Optional `networkx` engine (`pip install openwalkscore[networkx]`).
21
+ - **Data**: Overpass API client with timeouts, mirror fail-over, HTTP 429/5xx retry, on-disk
22
+ cache, process-wide rate limiting and the User-Agent `openwalkscore/<version>`. Offline
23
+ input from `.osm` XML, GeoJSON or saved Overpass JSON.
24
+ - **API**: `score_point`, `score_grid` (GeoJSON grid), `compare`, reusable `WalkScorer`,
25
+ explainable `WalkScoreResult` (subscores, counted amenities with distances, penalties,
26
+ network metrics, plain-text explanation).
27
+ - **Profiles**: built-in `default`, `family`, `senior`, `transit`; custom profiles from a
28
+ dict or TOML file, validated against typos.
29
+ - **CLI**: `openwalkscore score | grid | explain | compare | profiles`.
30
+ - **HTML maps**: self-contained Leaflet page (CDN with SRI hashes, OpenStreetMap tiles and
31
+ attribution), all text escaped.
32
+ - ESRI ASCII grid (`.asc`) reader for the slope penalty, or any `(lat, lon) -> metres`
33
+ callable.
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "yusufeminoglu@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "openwalkscore"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-08"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/openwalkscore_sdk"
13
+ url: "https://pypi.org/project/openwalkscore/"
14
+ abstract: "Open, transparent, API-key-free walkability scores (0-100) computed from OpenStreetMap: network-distance amenity access with distance decay and diminishing returns, plus street-connectivity and slope penalties."
15
+ keywords:
16
+ - walkability
17
+ - accessibility
18
+ - OpenStreetMap
19
+ - urban analytics
20
+ - 15-minute city
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/openwalkscore/py.typed
7
+ recursive-include src/openwalkscore *.py
8
+ recursive-include tests *.py
9
+ recursive-include tests/fixtures *.osm *.geojson *.asc *.toml
10
+ recursive-include examples *.py *.toml
@@ -0,0 +1,401 @@
1
+ Metadata-Version: 2.4
2
+ Name: openwalkscore
3
+ Version: 0.1.0
4
+ Summary: Open, transparent, API-key-free walkability scores (0-100) for any point or area, computed from OpenStreetMap.
5
+ Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/openwalkscore_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/openwalkscore_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/openwalkscore_sdk/issues
10
+ Project-URL: Documentation, https://github.com/YusufEminoglu/openwalkscore_sdk#readme
11
+ Project-URL: Changelog, https://github.com/YusufEminoglu/openwalkscore_sdk/blob/main/CHANGELOG.md
12
+ Keywords: walkability,walk-score,accessibility,15-minute-city,openstreetmap,overpass,urban-analytics,gis,proptech,real-estate,pedestrian,network-analysis,dijkstra,amenities
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Topic :: Scientific/Engineering :: GIS
23
+ Classifier: Topic :: Sociology
24
+ Classifier: Intended Audience :: Science/Research
25
+ Classifier: Intended Audience :: Developers
26
+ Classifier: Intended Audience :: Education
27
+ Classifier: Environment :: Console
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.9
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: certifi>=2023.7.22
33
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
34
+ Provides-Extra: networkx
35
+ Requires-Dist: networkx>=2.8; extra == "networkx"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
38
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
39
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
40
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
41
+ Requires-Dist: networkx>=2.8; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # openwalkscore
45
+
46
+ **Open, transparent, API-key-free walkability scores (0-100) for any point or area on Earth,
47
+ computed from OpenStreetMap.**
48
+
49
+ [![CI](https://github.com/YusufEminoglu/openwalkscore_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/openwalkscore_sdk/actions/workflows/ci.yml)
50
+ [![PyPI](https://img.shields.io/pypi/v/openwalkscore.svg)](https://pypi.org/project/openwalkscore/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/openwalkscore.svg)](https://pypi.org/project/openwalkscore/)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
53
+
54
+ `openwalkscore` measures how many everyday destinations (groceries, cafes, schools, parks,
55
+ clinics, transit stops and more) you can reach on foot, and how walkable the street network
56
+ around you is. Every number in the method is published below and lives in an editable
57
+ profile, so a score can always be reproduced, audited and adapted.
58
+
59
+ It is written for urban data scientists, real-estate and proptech developers, planners and
60
+ students who need a walkability indicator they can explain, and that works for any city.
61
+
62
+ > **Disclaimer.** openwalkscore is an independent open-source project. It is **not
63
+ > affiliated with, endorsed by or connected to Walk Score® or Redfin**. Walk Score® is a
64
+ > registered trademark of its owner. openwalkscore uses its own method and its own data
65
+ > (OpenStreetMap); **its scores are not comparable to Walk Score® values** and must not be
66
+ > presented as such.
67
+
68
+ ---
69
+
70
+ ## Contents
71
+
72
+ - [Install](#install)
73
+ - [Quick start](#quick-start)
74
+ - [Methodology](#methodology)
75
+ - [Profiles](#profiles)
76
+ - [Python API](#python-api)
77
+ - [Command line](#command-line)
78
+ - [Data sources and offline use](#data-sources-and-offline-use)
79
+ - [HTML maps](#html-maps)
80
+ - [Limitations](#limitations)
81
+ - [Citing](#citing)
82
+ - [License and attribution](#license-and-attribution)
83
+
84
+ ## Install
85
+
86
+ ```bash
87
+ pip install openwalkscore
88
+ # optional: run shortest paths with networkx instead of the built-in Dijkstra
89
+ pip install "openwalkscore[networkx]"
90
+ ```
91
+
92
+ Python 3.9 or newer. The only runtime dependency is `certifi` (plus `tomli` on Python < 3.11).
93
+ No API key, no account, no `osmnx`/`geopandas` stack required.
94
+
95
+ ## Quick start
96
+
97
+ ```python
98
+ from openwalkscore import score_point
99
+
100
+ r = score_point(48.8530, 2.3499) # Ile de la Cite, Paris
101
+ print(r.score, r.rating) # e.g. 100 Excellent
102
+ print(r.subscores) # {'grocery': 100.0, 'restaurants_cafes': 100.0, ...}
103
+ print(r.explain()) # plain-text, line-by-line justification
104
+
105
+ for hit in r.nearest_amenities(limit=5):
106
+ print(f"{hit.distance_m:5.0f} m {hit.category:<18} {hit.name}")
107
+ ```
108
+
109
+ The first call downloads amenities and the walking network around the point from the
110
+ public Overpass API (this can take up to a minute in a dense city centre) and caches the
111
+ response on disk; repeated calls are instant.
112
+
113
+ ```bash
114
+ openwalkscore score --lat 48.8530 --lon 2.3499
115
+ openwalkscore explain --lat 48.8530 --lon 2.3499
116
+ openwalkscore grid --bbox 2.340,48.850,2.360,48.860 --cell 200 -o paris.geojson --html paris.html
117
+ ```
118
+
119
+ ## Methodology
120
+
121
+ For a point *p* and a profile with categories *c* (weight *W_c*):
122
+
123
+ ### 1. Amenity categories
124
+
125
+ Each OpenStreetMap feature with a location (node, or way/relation centroid) is assigned to the
126
+ **first** category whose tag rules match (`shop=*` comes after the grocery and healthcare
127
+ rules, so a supermarket counts as grocery, not shopping). Default profile:
128
+
129
+ | Category | Weight | Counted (n) | Ratio (r) | Tags (abridged) |
130
+ |---|---:|---:|---:|---|
131
+ | Grocery | 3 | 2 | 0.4 | `shop=supermarket/convenience/greengrocer/bakery/butcher/deli`, `amenity=marketplace` |
132
+ | Restaurants & cafes | 3 | 10 | 0.8 | `amenity=restaurant/cafe/fast_food/pub/bar/food_court/ice_cream` |
133
+ | Healthcare | 1 | 2 | 0.5 | `amenity=pharmacy/clinic/doctors/hospital/dentist`, `shop=chemist`, `healthcare=*` |
134
+ | Shopping | 2 | 5 | 0.7 | `shop=*` (except vacant, car, car_parts, fuel) |
135
+ | Schools | 1 | 1 | - | `amenity=school/kindergarten/childcare/college/university` |
136
+ | Parks & play | 1 | 2 | 0.5 | `leisure=park/playground/garden/recreation_ground/nature_reserve` (not `access=private`) |
137
+ | Banks & post | 1 | 1 | - | `amenity=bank/atm/bureau_de_change/post_office` |
138
+ | Culture & community | 1 | 2 | 0.5 | `amenity=library/theatre/cinema/arts_centre/community_centre`, `tourism=museum/gallery` |
139
+ | Transit stops | 2 | 3 | 0.5 | `highway=bus_stop`, `public_transport=platform`, `railway=station/halt/tram_stop/subway_entrance`, `amenity=ferry_terminal` |
140
+
141
+ Run `openwalkscore explain` (no coordinates) to print the complete profile as JSON.
142
+
143
+ ### 2. Walking distance
144
+
145
+ * The walking network is built from OSM ways with a walkable `highway=*` value (footway,
146
+ path, pedestrian, steps, living_street, residential, service, tertiary ... primary, track,
147
+ cycleway, ...). Motorways and trunk roads are excluded unless `foot=yes`; `foot=no`,
148
+ `access=private` and `foot=use_sidepath` are respected.
149
+ * Every pair of consecutive way nodes is an undirected edge weighted by its great-circle
150
+ length.
151
+ * The scored point and every amenity are **snapped to the nearest edge** (not node), and the
152
+ perpendicular access legs are added to the route.
153
+ * Shortest paths come from a pure-Python multi-source **Dijkstra** that starts part-way along
154
+ the origin edge and stops at the decay cut-off (1.6 km). `engine="networkx"` gives
155
+ identical results with networkx.
156
+ * Fallback: amenities more than `max_snap_m` (250 m) from the network, or every amenity when
157
+ the point itself has no network nearby, use **straight-line distance x detour factor 1.3**.
158
+ The result reports `distance_mode` (`network`, `straight-line` or `mixed`) per amenity
159
+ and overall.
160
+ * An amenity that is close but not connected to the point's network (for example across a
161
+ river with no bridge in the data) gets no credit.
162
+
163
+ ### 3. Distance decay
164
+
165
+ Credit for an amenity at walking distance *d*:
166
+
167
+ ```
168
+ f(d) = 1 if d <= 400 m
169
+ f(d) = 0.5 * (1 + cos(pi * t)) if 400 m < d < 1600 m, t = (d - 400) / 1200
170
+ f(d) = 0 if d >= 1600 m
171
+ ```
172
+
173
+ The cosine taper is smooth at both ends: an amenity 700 m away earns 0.85, 1 km away 0.50,
174
+ 1.3 km away 0.15. A linear ramp is available (`decay.shape = "linear"`).
175
+
176
+ ### 4. Diminishing returns
177
+
178
+ Within a category the amenities are ranked by credit and the best *n* are kept. The *k*-th
179
+ one is weighted by
180
+
181
+ ```
182
+ s_k = r^(k-1) / (1 + r + ... + r^(n-1))
183
+ ```
184
+
185
+ so the shares sum to 1 and each extra cafe adds less than the previous one. The category
186
+ subscore is `S_c = sum_k s_k * f(d_k)`, between 0 and 1. Example (restaurants & cafes,
187
+ n = 10, r = 0.8): one cafe next door is worth 22 % of the category, five cafes 75 %, ten
188
+ cafes 100 %.
189
+
190
+ ### 5. Access score
191
+
192
+ ```
193
+ A = 100 * sum_c W_c * S_c / sum_c W_c
194
+ ```
195
+
196
+ ### 6. Network penalties
197
+
198
+ Measured within `analysis_radius_m` (800 m) of the point:
199
+
200
+ | Penalty | Metric | No penalty at | Full penalty at | Max |
201
+ |---|---|---:|---:|---:|
202
+ | Intersection density | nodes where >= 3 street edges meet, per km² | >= 90 /km² | <= 30 /km² | 10 % |
203
+ | Block length | mean length of street segments between intersections / dead ends | <= 120 m | >= 240 m | 10 % |
204
+ | Slope (only with a DEM) | length-weighted mean absolute grade of those segments | <= 3 % | >= 15 % | 10 % |
205
+
206
+ Each penalty is linear between its two thresholds. They combine multiplicatively,
207
+ `P = 1 - prod_i (1 - P_i)`, capped at `max_total_penalty` (30 %). Penalties are skipped
208
+ (and the explanation says so) when the point has no network nearby.
209
+
210
+ ### 7. Final score
211
+
212
+ ```
213
+ score = round(A * (1 - P)) # integer 0..100
214
+ ```
215
+
216
+ | Score | Rating |
217
+ |---:|---|
218
+ | 90-100 | Excellent |
219
+ | 70-89 | High |
220
+ | 50-69 | Moderate |
221
+ | 25-49 | Low |
222
+ | 0-24 | Very low |
223
+
224
+ ## Profiles
225
+
226
+ Built-in profiles: `default`, `family` (schools, parks and healthcare weighted up),
227
+ `senior` (300 m full credit, 1 km cut-off, healthcare and transit weighted up, stronger slope
228
+ penalty) and `transit` (transit stops dominate).
229
+
230
+ A custom profile overrides a base profile key by key, from a dict or a TOML file:
231
+
232
+ ```toml
233
+ # nightlife.toml
234
+ [profile]
235
+ name = "nightlife"
236
+ base = "default"
237
+
238
+ [profile.decay]
239
+ full_m = 300
240
+ zero_m = 1200
241
+ shape = "linear"
242
+
243
+ [profile.categories.restaurants_cafes]
244
+ weight = 6
245
+ max_count = 12
246
+
247
+ [profile.categories.nightclubs] # a new category
248
+ weight = 2
249
+ tags = ["amenity=nightclub", "amenity=music_venue"]
250
+ max_count = 3
251
+ ratio = 0.6
252
+
253
+ [profile.penalties.slope]
254
+ max_penalty = 0.15
255
+ ```
256
+
257
+ ```python
258
+ from openwalkscore import Profile, score_point
259
+
260
+ p = Profile.from_toml("nightlife.toml")
261
+ p = Profile.from_dict({"categories": {"shopping": {"weight": 0}}}) # switch a category off
262
+ score_point(52.52, 13.405, profile=p)
263
+ score_point(52.52, 13.405, profile="nightlife.toml") # a path works too
264
+ ```
265
+
266
+ Unknown keys raise `ValueError`, so a typo never silently changes a score. Set
267
+ `replace_categories = true` to start from an empty category list.
268
+
269
+ ## Python API
270
+
271
+ | Function / class | Purpose |
272
+ |---|---|
273
+ | `score_point(lat, lon, profile="default", *, data=None, dem=None, engine="python", client=None, use_cache=True, name="")` | Score one point; returns `WalkScoreResult`. |
274
+ | `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. |
275
+ | `compare(points, profile="default", *, data=None, ...)` | Score `(lat, lon)`, `(name, lat, lon)` or `{"name", "lat", "lon"}` items; close points share one download. |
276
+ | `WalkScorer(data, profile, dem=None, engine="python")` | Build the network once, call `.score(lat, lon)` / `.grid(bbox, cell_m)` many times. |
277
+ | `WalkScoreResult` | `score`, `rating`, `access_score`, `total_penalty`, `subscores`, `categories` (with counted `AmenityHit`s), `penalties`, `metrics`, `distance_mode`, `explain()`, `nearest_amenities()`, `to_dict()`, `to_geojson_feature()`. |
278
+ | `Profile`, `get_profile`, `list_profiles` | Profiles and their configuration. |
279
+ | `OverpassClient(endpoints, timeout_s, cache_dir, cache_ttl_s, min_interval_s, retries)` | Download control. |
280
+ | `load_osm(path_or_obj)` | Read `.osm`, `.geojson`, Overpass `.json`, or a dict. |
281
+ | `WalkNetwork`, `dijkstra` | The graph and the shortest-path routine, usable on their own. |
282
+ | `DEM.from_ascii_grid(path)`, `DEM.from_function(fn)` | Elevation for the slope penalty. |
283
+ | `render_html(obj)`, `write_html(obj, path)` | Leaflet map of a result, a list of results or a grid. |
284
+
285
+ ```python
286
+ from openwalkscore import WalkScorer, load_osm
287
+
288
+ scorer = WalkScorer(load_osm("district.osm"), profile="family")
289
+ results = [scorer.score(lat, lon) for lat, lon in listings] # fast: graph built once
290
+ ```
291
+
292
+ Using a DEM:
293
+
294
+ ```python
295
+ score_point(41.03, 28.98, dem="istanbul_wgs84.asc") # ESRI ASCII grid in degrees
296
+
297
+ import rasterio # or any sampler you like
298
+ src = rasterio.open("dem_wgs84.tif"); band = src.read(1)
299
+ def elevation(lat, lon):
300
+ row, col = src.index(lon, lat)
301
+ return float(band[row, col])
302
+ score_point(41.03, 28.98, dem=elevation)
303
+ ```
304
+
305
+ ## Command line
306
+
307
+ ```text
308
+ openwalkscore score --lat LAT --lon LON [--profile P] [--json] [--html map.html]
309
+ openwalkscore explain [--lat LAT --lon LON] [--profile P]
310
+ openwalkscore grid --bbox WEST,SOUTH,EAST,NORTH --cell 250 -o grid.geojson [--html map.html]
311
+ openwalkscore compare --point "Home,41.03,28.98" --point "Office,41.04,29.00" [--json]
312
+ openwalkscore profiles
313
+ ```
314
+
315
+ Common options: `--input FILE` (offline `.osm`/`.geojson`/`.json`), `--dem FILE.asc`,
316
+ `--engine python|networkx`, `--timeout SECONDS`, `--endpoint URL` (repeatable),
317
+ `--no-cache`, `--cache-dir DIR`. Exit codes: 0 ok, 2 invalid input, 3 download failed.
318
+
319
+ `explain` without coordinates prints the method summary and the full profile.
320
+
321
+ ## Data sources and offline use
322
+
323
+ **Overpass API (default).** `OverpassClient` sends one query per area (amenities with
324
+ `out center`, plus the walkable ways and their nodes) to public Overpass instances, trying
325
+ `overpass-api.de`, `overpass.kumi.systems` and `overpass.private.coffee` in turn. It:
326
+
327
+ * sets the generic User-Agent `openwalkscore/<version>`;
328
+ * applies a network timeout (default 60 s) and retries HTTP 429/502/503/504 with back-off
329
+ before failing over to the next mirror;
330
+ * waits at least 1 s between requests from the same process;
331
+ * caches responses on disk for 7 days (`%LOCALAPPDATA%\openwalkscore\cache`,
332
+ `~/.cache/openwalkscore`, or `OPENWALKSCORE_CACHE_DIR`);
333
+ * refuses areas with a diagonal above 25 km. For a whole city, download an extract once
334
+ (e.g. from Geofabrik, then clip with `osmium`) and pass it with `data=` / `--input`.
335
+
336
+ Please follow the [Overpass usage policy](https://dev.overpass-api.de/overpass-doc/en/preface/commons.html):
337
+ keep areas small and avoid parallel bulk jobs.
338
+
339
+ **Offline input.** `data=` (or `--input`) accepts:
340
+
341
+ * `.osm` / `.xml` - OSM XML (JOSM, OSM API, `osmium cat -f osm`);
342
+ * Overpass JSON (`{"elements": [...]}`) saved from overpass-turbo or the API;
343
+ * GeoJSON - points become amenities, `LineString`s with a `highway` property become the
344
+ network (touching vertices are joined), polygons become amenities at their centroid.
345
+ Tags are read from `properties` or `properties.tags`.
346
+
347
+ ## HTML maps
348
+
349
+ ```python
350
+ from openwalkscore import score_point, score_grid, write_html
351
+ write_html(score_point(40.4168, -3.7038), "madrid.html")
352
+ write_html(score_grid((-3.71, 40.41, -3.69, 40.425), 200), "madrid_grid.html")
353
+ ```
354
+
355
+ The page loads Leaflet 1.9.4 from unpkg with Subresource Integrity hashes and shows
356
+ OpenStreetMap tiles with the required attribution. Data is embedded as JSON with `<`, `>`
357
+ and `&` escaped, and all OSM-supplied text (names, tags) is inserted with `textContent`, so
358
+ feature names cannot inject markup or script.
359
+
360
+ ## Limitations
361
+
362
+ * **OSM completeness varies.** Missing shops or footpaths lower the score; a score is a
363
+ statement about the map as much as about the place. Compare places within regions of
364
+ similar mapping quality.
365
+ * **Sidewalks mapped as separate footways** add intersections and short segments; the
366
+ density and block-length penalties are therefore lenient in such areas by design (they
367
+ rarely trigger where sidewalks are mapped). Penalties near the edge of a local extract
368
+ can be overstated if the extract does not cover `analysis_radius_m`.
369
+ * No crossing safety, traffic speed, sidewalk width, lighting, opening hours, shade,
370
+ crime or amenity quality is modelled.
371
+ * The network is treated as undirected and of uniform walking speed; steps and steep grades
372
+ only enter through the optional slope penalty.
373
+ * Amenity counts are capped per category; a mall with 200 shops counts as at most 5 shops
374
+ in the default profile.
375
+
376
+ ## Development
377
+
378
+ ```bash
379
+ git clone https://github.com/YusufEminoglu/openwalkscore_sdk
380
+ cd openwalkscore_sdk
381
+ pip install -e ".[dev]"
382
+ python -m pytest # no network needed: tests use hand-made OSM fixtures
383
+ python -m ruff check . && python -m ruff format --check src tests && python -m mypy src
384
+ ```
385
+
386
+ ## Citing
387
+
388
+ If you use openwalkscore in research, please cite it via [CITATION.cff](CITATION.cff):
389
+
390
+ > Eminoğlu, Y. (2026). *openwalkscore: open, transparent walkability scores from
391
+ > OpenStreetMap* (version 0.1.0) [Software]. https://github.com/YusufEminoglu/openwalkscore_sdk
392
+
393
+ ## License and attribution
394
+
395
+ Code: MIT, © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
396
+
397
+ Map data: © [OpenStreetMap contributors](https://www.openstreetmap.org/copyright), available
398
+ under the Open Database License (ODbL). When you publish scores or maps derived from OSM
399
+ data, credit OpenStreetMap accordingly.
400
+
401
+ Walk Score® is a registered trademark of its owner; this project is not affiliated with it.