# South Africa (ZA) — internal licensing notes

## Source research, 2026-08-28 — sellability + reproducibility audit

**GeoNames is the only source that is both legally sellable and reproducible —
same shape of finding as Albania — but here it carries a much larger gap: it
has ZERO administrative-division data for South Africa, not a reduced amount.**

### The only usable source — GeoNames ZA

```
https://download.geonames.org/export/zip/ZA.zip       # 26 KB, 3,920 rows
```

Licence **CC-BY-4.0**, verified against `export/zip/readme.txt` on
2026-08-28 — South Africa carries no per-country caveat.

**Verified directly against the raw source file (not the corrupted R2
export):** `admin_name1`, `admin_name2`, and `admin_name3` are empty on
**every single one of the 3,920 rows** in GeoNames' own `ZA.txt`. This is
not a pandas-round-trip bug like TH/RO/AL — GeoNames genuinely carries no
province, district, or municipality data for South Africa at all. Only
`postal_code`, `place_name`, `latitude`, `longitude`, `accuracy` are ever
populated. 3,266 distinct postal codes across 952 distinct place names — the
coordinates are coarse city/town-level points (e.g. every Pretoria-area code
from 0002–0006 shares the exact point `-25.7069, 28.2294`), not per-code
locations.

### The official source — SAPO — is explicitly closed, not merely unresolved

The South African Post Office (SAPO) maintains the authoritative postal code
list but its own website terms of use require **prior written permission for
commercial use** — this was checked directly (Open Knowledge's Global Open
Data Index review of SA postcodes, 2015, quotes SAPO's own ToU to this
effect) and is a clean `decision: "incompatible"`, not `"unresolved"`. Also
not machine-readable in bulk (XLS only, not a downloadable dataset). Do not
re-attempt scraping SAPO for a commercial product.

### Ruled out — do not re-attempt without new evidence

| Candidate | Verdict | Why |
|---|---|---|
| **SAPO official list** | ❌ **incompatible** | Own ToU requires prior written permission for commercial use (verified via Open Knowledge Index citation of SAPO's terms). Not bulk-downloadable in any case. |
| **AfriGIS** | ❌ not sellable | Privately owned commercial dataset; no open licence. |
| **opendb.co.za** | ❌ unresolved, and smaller | Downloadable CSV (1,357 rows — fewer than GeoNames' 3,920) with province/municipality fields, but its own Terms and Conditions page states nothing about licensing data downloads and nowhere discloses where OpenDB itself sources the data. Same shape as Mauritius' youbianku.com case: a genuine absence of stated terms, not a confirmed grant — `decision: "unresolved"` at best, and the row-count regression makes it a poor trade even if accepted. |
| **raramuridesign-cc/South-African-Postal-Codes** (GitHub) | ❌ licence-laundering risk | Repo itself carries a GPL-3.0 LICENSE file, but nowhere states where `postcodes-za.csv` was originally sourced. A GPL licence on the *repository* cannot grant rights the uploader never held over the *data*, and GPL is also a poor fit for data redistribution generally (copyleft software licence, not a data licence). Same failure shape as the TH kongvut/Albania-HDX traps already documented in this catalog: an asserted permissive container licence over unverified data provenance. |
| **DataWookie/saffeR** (GitHub R package) | ❌ same trap | MIT-licensed package, but its `postalcodes` dataset (province, area, suburb, boxcode) has no cited source anywhere in the README or docs — the only postal-code link in the repo points to Wikipedia's postal-codes article as a *suggested reference for readers*, not as the dataset's actual provenance. |
| **Wikipedia "List of postal codes in South Africa"** | ❌ doesn't exist as a list | Despite the title, this page contains no per-code or per-range table — it redirects to the general "Postal codes in South Africa" article, which only explains the format with two or three illustrative examples. Nothing to extract. |
| **Municipal Demarcation Board (MDB)** | ⚠️ different geography, not evaluated further | Publishes real official municipal/ward boundary shapefiles, but these are administrative boundaries, not postal boundaries, and don't align with postal code areas. Using them would require a full point-in-polygon spatial join against postal-code coordinates — and GeoNames' own ZA coordinates are too coarse for that to be meaningful (952 distinct points shared across 3,920 codes). MDB's own redistribution licence was not confirmed (site's terms-of-use page did not resolve during this research pass — cert error). This is a large, separate geocoding-style build, not a quick fix; not attempted this pass. |
| **HDX — South Africa Subnational Administrative Boundaries** (`cod-ab-zaf`) | ⚠️ not checked, flagged by pattern | Page returned HTTP 403 during this research pass, so its `methodology`/licence fields could not be verified directly. Given Albania's HDX entry in this same catalog traced back to GADM (non-commercial) despite a permissive package-level licence label, treat this with the same suspicion until actually read — do not assume it is clean. |

## What this means for the product

A GeoNames-only rebuild is legally sellable (CC BY 4.0, unambiguous) and
would fix the current file's defects, but the result is a **much thinner
product than usual**: `postal_code`, `place_name`, `latitude`, `longitude`,
`accuracy` only — every `admin_name*`/`admin_code*` field ships empty, not
because of a build limitation but because no commercially licensable source
carries South African admin-tier data at all. This is a genuine product-
quality ceiling, not a licensing block.

## Defects in the currently-published file

`za/za.csv` on R2 is a GeoNames re-export (row count matches `ZA.zip`
exactly: 3,920) round-tripped through pandas and corrupted:

1. **`admin_name1`/`admin_name2`/`admin_name3` are the literal string `nan`
   on every row.** As established above, GeoNames' own source has these
   genuinely empty — the `nan` string is the round-trip artifact, but the
   underlying emptiness itself is real and does not resolve with any
   licensable source found.
2. Integer `accuracy` written as float (`4` → `4.0`); many rows blank.
3. 3,920 rows but only **3,266 distinct postal codes** — check duplicates
   against the primary key rule before publishing (distinct on
   `(postal_code, place_name)` is expected to be clean; verify at build time).

**Attribution is absent from the sold product.** R2 carries only
`za/za.csv` — no `README.md`, `datapackage.json`, `ATTRIBUTION.txt` or
`za.zip`. The repo `datapackage.yml` lists `sources: [{title: "Multiple
sources"}]` with no `licenses[]`. CC BY 4.0 attribution is a licence
condition — live breach on a paid product.

`datapackage.yml` also sets `is_premium: true` with **no `premium_tier` and
no `premium_source_key`**, and no zip exists in R2 for a key to point at.

## licence_status (proposed — needs human sign-off before shipping)

```json
{
  "channel": "postal-codes-r2",
  "components": [
    {
      "field_group": "postal_code, place_name, latitude, longitude, accuracy",
      "source_url": "https://download.geonames.org/export/zip/ZA.zip",
      "licence_id": "CC-BY-4.0",
      "collected_at": "2026-08-28",
      "decision": "compatible",
      "decided_by": "claude-session-2026-08-28",
      "decided_at": "2026-08-28"
    },
    {
      "field_group": "admin_name1/2/3, admin_code1/2/3",
      "source_url": null,
      "licence_id": null,
      "collected_at": "2026-08-28",
      "decision": "no_source_found",
      "decided_by": "claude-session-2026-08-28",
      "decided_at": "2026-08-28",
      "note": "Not 'unresolved' in the usual sense of an unread ToU -- every candidate source was checked and individually ruled out (SAPO incompatible, others unresolved-provenance or wrong-geography). Ships empty, not guessed."
    }
  ]
}
```

## Proposed tier

**B ($49) at most**, and worth a deliberate human decision on whether B is
even justified given how thin the product is (5 of 13 columns real content,
0 of 8 admin columns populated). This is a weaker product than AL (which at
least carries qark+bashki), despite ZA usually being thought of as a bigger,
more developed market. Do not price it as A — there is no second source
consolidated onto it, only a single-source cleanup, and the admin gap is not
something a human sign-off can wave through since no licensable source
exists to fill it.

## Verdict (superseded below — see Build, 2026-08-29)

**Possible, but weak.** GeoNames-only is buildable and legally sellable
today — same mechanical fix as TH/RO/AL (rebuild via `csv`, not pandas, fix
the `nan` literals and float accuracy). It should NOT be marketed as
carrying province/municipality data, because it doesn't and no licensable
source was found that provides it. If a real admin tier is wanted later, the
MDB spatial-join path is the only remaining lead, and it is a substantial
separate build (geocoding-shaped, not a normalisation), not a quick follow-up.

## Build — 2026-08-29 (local only, not published)

Human asked to fill the admin-tier gap using AfriGIS. Checked first: AfriGIS
is a paid, API-key-gated commercial geocoding service
(developers.afrigis.co.za, 50M+ address records), not a downloadable
dataset — no API credentials exist anywhere in this environment (`.env`,
`.env.template` checked), and bulk-querying a commercial geocoding API to
build a cached, resold reference dataset is the same shape of ToS
violation this project already blocks for Nominatim (see `CLAUDE.md`'s
OSM/Nominatim gate — "systematic queries... reselling of geocoding
results" is expressly prohibited by that class of provider). Presented
this to the human; instruction was to skip AfriGIS and look for other
sources instead.

**Found one, and it was hiding in a source already in hand.** GeoNames
publishes TWO separate exports: the postal export (`export/zip/ZA.zip`,
zero admin fields, already established above) and a general-purpose
gazetteer dump (`export/dump/ZA.zip`) — same publisher, same CC BY 4.0
licence, not a new source or a new licensing question. The dump DOES carry
full South African admin data: 9 provinces and 52 district/metro
municipalities, via `admin1CodesASCII.txt` / `admin2Codes.txt`, populated
on 12,875 of its own populated-place entries.

Built a same-source join: postal row's place name matched against the
gazetteer (disambiguating common names that repeat across provinces by
nearest coordinate), falling back to pure nearest-neighbour search where no
name matches at all. Measured directly before shipping:

| | value |
|---|---|
| Final rows | 3,920 (unchanged from source) |
| Distinct postal codes | 3,266 |
| Exact-name-match rows (median 1.2 km, p90 5.9 km from matched point) | 2,879 |
| Coordinate-fallback rows, no name match (median 1.1 km, max 44.1 km) | 1,041 |
| Rows left unresolved | 0 |
| Provinces covered | 9/9 |
| Districts/metro municipalities covered | 52/52 |

Verified the join on a spot check (postal code 0002/0003/0004/0005,
Pretoria → Gauteng → City of Tshwane Metropolitan Municipality) before
trusting it at scale.

**This is disclosed as a coordinate-based join against GeoNames' own
gazetteer, not an official South African administrative register** — high
confidence, not authoritative in the register sense. Stated explicitly in
`README.md`/`ATTRIBUTION.txt`/`datapackage.json`, not silently upgraded to
look like government data. `admin_code2` values are GeoNames' own internal
identifiers (e.g. "DC32", "ETH"), not an independently verified official
municipal code registry — also disclosed rather than presented as
authoritative.

`admin_code1` derived to ISO 3166-2:ZA via a verified crosswalk (Wikipedia,
checked 2026-08-29) from GeoNames' internal admin1 numbering — GeoNames'
own admin1 codes (`ZA.02`, `ZA.03`, ...) are not themselves ISO codes.

Frictionless validation: `valid: True`.

## Verdict (final)

**Ship it as tier B, with the join method disclosed.** This is a
meaningfully stronger product than the original GeoNames-only proposal —
9/9 provinces and 52/52 districts recovered, not zero — at no new
licensing cost (same publisher, same CC BY 4.0). Still single-source in
the tier sense (both exports are GeoNames), so **B ($49)**, not A: real
derivation work (the gazetteer join) but no second authoritative provider
consolidated onto it.

**Status: built and validated locally only.** Nothing published to R2,
nothing committed to git. `datasets/za/README.md` and `datapackage.yml`
updated in place (tier B, `premium_source_key: za/za.zip`, real
`sources`/`licenses`, replacing the "Multiple sources" placeholder and the
broken `is_premium: true` with no tier/key that was live on R2 before this
build). The previously-flagged live breach (broken paid product, missing
attribution files) is fixed in this local build but the fix has not been
published — R2 still serves the old defective file until someone runs the
publish step.
