An interactive map of the ~909 official zones where non-Saudis may own property under Saudi Arabia's new foreign-ownership law (Royal Decree M/14), built entirely from the public REGA regulator API. A small Python pipeline turns REGA's raw zone records into a web-ready GeoJSON — reconciling REGA's mixed m²/km² area units against each zone's own polygon — and a dependency-light, vendored-Leaflet front-end renders both a detailed category map and a regional choropleth.
Most "map of the new law" write-ups are a screenshot of a press list. This is the regulator's own complete dataset, extracted from REGA's official JSON API, turned into a reproducible pipeline and a polished map — with the unglamorous data-quality problems solved honestly instead of papered over.
- The regulator contradicts its own units — and we don't guess. REGA stores each zone's area in mixed units (m² for some records, km² for others, no structural rule), so the raw number is ambiguous by a factor of a million. The build picks the interpretation that matches REGA's own published polygon, uses the geometry only to choose the unit, and still reports REGA's stated figure — never a recomputation.
- It reports the regulator's mistakes rather than silently "fixing" them. In 6 zones REGA's stated area disagrees with its own polygon by more than 15%; those are flagged and published as-is, with the polygon area alongside for reference.
- Names are unreliable, so it joins on codes. REGA and the boundary source spell several provinces differently (Hail/Hayel, Asir/'Asir, Makkah / Makkah Al Mukarramah). Region aggregation joins on the ISO 3166-2:SA code, not the name.
- A screenshot-ready slide graphic with no basemap. The regional overview drops the raster tiles entirely: a designed gradient "sea", muted Natural Earth neighbour silhouettes clipped to the peninsula, and the Kingdom lifted with a drop-shadow — so a single screenshot is a clean slide with no foreign city labels.
- No build tooling, no CDN JavaScript. Leaflet is vendored locally; the front-end is three static files of vanilla JS/CSS.
No hand-waving — this is exactly what the pipeline does.
| Problem | How it's solved (in code) |
|---|---|
Ambiguous area units (build_geojson.py) |
rega_area_km2() returns min((za, za/1e6), key=lambda v: abs(log(v / polygon_km2))) — of the two possible readings of REGA's zoneArea, it keeps the one whose log-ratio to the polygon area is smallest. The polygon disambiguates the unit; the value returned is REGA's own. Unknown → None/n/a. |
| Polygon area on a sphere | A spherical shoelace over each ring: s += radians(Δlon)·(2 + sin(lat₁) + sin(lat₂)), then `area = |
| REGA self-conflicts | A public zone is flagged when ` |
Region name mismatch (build_regions.py) |
REGA's region label → ISO 3166-2:SA code (REGA_TO_ISO), then matched to the boundary file's shapeISO. Asserts every REGA region maps and every mapped region has ≥1 zone, so a spelling drift fails loudly instead of silently dropping zones. |
| Slide-legible labels | Per-province label anchors are hand-tuned (MANUAL_LABEL) to avoid collisions at country zoom; provinces fall back to the largest landmass's representative point so labels never land on an offshore islet. Smaller provinces get a higher zIndexOffset so their pills stack on top. |
| File weight | Zone coordinates rounded to 5 dp (~1 m); region boundaries shapely.simplify(0.01°, preserve_topology=True) (~1.1 km, sub-pixel at country zoom) and rounded to 4 dp. |
The two front-end maps are pure Leaflet:
index.html— the detailed map, one layer per REGA category with a clickable legend, EN/AR/code search, and a canvas renderer for the ~900 polygons. The two whole-city "comprehensive development" envelopes (L057Riyadh,L058Jeddah) are held in anOMITset so the specific zones inside them stay legible; they remain inzones.geojsonand the workbook.regions.html— a 13-region choropleth with a By zones / By area toggle. Each metric carries its own sequential ramp and thresholds (a value lands in bucket i whenv ≥ br[i]), e.g. zonesbr = [0,20,45,75,105,130], areabr = [0,1000,2000,4000,8000,20000]. Dedicated Leaflet panes stack the neighbour context below the Kingdom, and the drop-shadow is applied to the whole SVG pane so only the country silhouette is shadowed.
REGA "Saudi Properties" portal API (public JSON, geometry included)
│ fetched once, saved verbatim
▼
data/rega_zones_api_raw.json + data/official_zones_full_930.json
│
├───────────────► build_geojson.py ──────► public/zones.geojson
│ unit disambiguation, (909 zones, minified)
│ 5-dp coord rounding
│
├───────────────► build_reference_workbook.py ──► output/*.xlsx
│ Zones · Summary (live COUNTIFS/SUMIFS) · Source & Method
│
└─ + data/sau_adm1_geoboundaries.geojson (geoBoundaries, CC BY 4.0)
+ data/ne_110m_admin_0_countries.geojson (Natural Earth, public domain)
│
▼
build_regions.py ─────► public/regions.geojson (13 provinces + stats)
ISO 3166-2 join, public/context.geojson (clipped neighbours)
shapely simplify,
per-region aggregates
│
▼
public/ static site — vanilla JS + vendored Leaflet (no CDN JS)
index.html detailed category map (CARTO Positron basemap)
regions.html regional choropleth (custom no-basemap slide graphic)
- Build scripts (
build_geojson.py,build_regions.py,build_reference_workbook.py) — pure Python; onlyshapely(region geometry) andopenpyxl(the Excel workbook) are third-party. Every path resolves relative to the script, or viaREGA_DATA_DIR/OUT_DIR. public/— the deployable static site. The rendered GeoJSON payload is shipped, so the maps run without rebuilding.docs/REGA_data_provenance_briefing.md— a full provenance/method note (source, access method, the 909-vs-930 split, the area-unit rule, the 6 self-conflicts, reproducibility).
Python 3 · shapely · openpyxl · GeoJSON · Leaflet.js (vendored) · vanilla JS/HTML/CSS · CARTO Positron basemap · geoBoundaries / Natural Earth
The regional overview renders out of the box (its small aggregate payload ships with the repo):
python3 -m http.server --directory public 8083
# open http://localhost:8083/regions.html (regional overview — works immediately)The detailed zone map (index.html) needs the full zone dataset, which is not bundled — build it first from the public REGA source (one command, below). Then http://localhost:8083 shows the detailed map.
To build the data from source:
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# 1. Put the source files in ./data (or set REGA_DATA_DIR):
# rega_zones_api_raw.json — REGA API dump, 909 public zones WITH geometry
# official_zones_full_930.json — REGA API dump, all 930 zones
# sau_adm1_geoboundaries.geojson — geoBoundaries gbOpen SAU ADM1 (CC BY 4.0)
# ne_110m_admin_0_countries.geojson — Natural Earth 110m admin-0 (public domain)
# (how to fetch the REGA dumps is in docs/REGA_data_provenance_briefing.md)
# 2. Build
python3 build_geojson.py # -> public/zones.geojson
python3 build_regions.py # -> public/regions.geojson + context.geojson
python3 build_reference_workbook.py # -> output/REGA_KSA_Ownership_Zones.xlsxEnvironment variables (see .env.example): REGA_DATA_DIR, OUT_DIR.
This repository contains code only — no bundled datasets. The full REGA zone dataset (public/zones.geojson) is not shipped; the pipeline reconstructs it from REGA's public, unauthenticated API, and docs/REGA_data_provenance_briefing.md documents exactly how to fetch the source JSON so the build is fully reproducible. A small aggregated regional payload (province outlines) is kept only so the regional overview renders as a demo; it is derived entirely from open, publicly-licensed geodata — geoBoundaries (CC BY 4.0) and Natural Earth (public domain). Leaflet is vendored under its BSD-2 licence. There is no client, proprietary, or personal data anywhere in the repo.
build_geojson.py REGA raw JSON -> public/zones.geojson (unit disambiguation)
build_regions.py per-region aggregates + choropleth geometry + neighbour context
build_reference_workbook.py styled Excel workbook (Zones / Summary / Source & Method)
public/ static site: index.html, regions.html, vendored Leaflet, *.geojson
docs/ data-provenance & method note
MIT — see LICENSE.
