A downloaded layer has no licence you can find
Problem statement
The layer is in the project, the map is nearly finished, and nobody can say what licence it is under. The download page has gone, the zip has no licence file, the GeoPackage has no metadata table, and the person who fetched it left in March.
This is not a rare edge case; it is the normal end state of any dataset acquired without a record. The practical consequence is specific: an output built on an unlicensed layer cannot be published, and the longer the layer stays in the project the more expensive removing it becomes.
This guide covers what to check, in the order that costs least, and what to do when the answer is genuinely unavailable.
Quick answer
Look in the six places a licence hides before asking anyone:
import pathlib, zipfile, json, sqlite3
from osgeo import gdal
gdal.UseExceptions()
def find_licence(path):
path, found = pathlib.Path(path), {}
ds = gdal.OpenEx(str(path))
found["dataset_metadata"] = ds.GetMetadata() # 1. inside the file
for sidecar in path.parent.glob(path.stem + "*"): # 2. sidecars
if sidecar.suffix.lower() in {".xml", ".txt", ".json", ".yaml", ".md"}:
found.setdefault("sidecars", []).append(sidecar.name)
for name in ("LICENSE", "LICENCE", "COPYING", "README", "terms", "TERMS"):
for hit in path.parent.glob(f"**/*{name}*"): # 3. the folder
found.setdefault("licence_files", []).append(str(hit))
for z in path.parent.glob("*.zip"): # 4. the original zip
with zipfile.ZipFile(z) as zf:
found.setdefault("zip_contents", {})[z.name] = zf.namelist()[:20]
return found
Then check the layer's attributes for a source or copyright column, and the project file for the URL it was loaded from.
Step-by-step solution
1. Look inside the file
GeoPackages have a metadata table, GeoTIFFs have tags, FileGDBs have per-item XML, and NetCDF has global attributes. A LICENSE, COPYRIGHT, ATTRIBUTION or SOURCE item may be sitting there.
2. Look for sidecars and the original archive
A .shp.xml, a README.txt in the zip, a terms.pdf that got left in downloads/. If the zip is still around, its contents list often names the publisher even when no licence file survived.
3. Look at the attributes
Many national datasets carry a source, licence or copyright column, or an identifier whose format is distinctive โ a UPRN, a GEOID, an INSPIRE ID, an ONS code. That identifies the publisher even when nothing else does.
4. Identify the publisher from the data itself
The CRS, the extent, the schema and the geometry precision narrow it fast. A layer in EPSG:27700 with LSOA21CD columns is ONS; one in EPSG:3035 with NUTS codes is Eurostat; one with osm_id is OpenStreetMap-derived and therefore ODbL.
import geopandas as gpd
gdf = gpd.read_file(path)
print(gdf.crs, sorted(gdf.columns), gdf.total_bounds)
5. Check the project and the history
A QGIS project stores the path a layer was loaded from; a notebook records the URL; a shell history or a download folder holds the file's origin. Version control on the pipeline is the most reliable of these.
6. Ask, with a deadline
If the publisher is identified, the licence is usually one page away. If the publisher is not identified, ask internally once, with a date after which the layer is removed.
7. If you cannot resolve it, replace it
Treat an unlicensed layer as unusable. In most cases an open equivalent exists โ administrative boundaries, addresses, roads, land cover all have open sources โ and swapping it is cheaper than defending a publication built on it.
8. Make the next download self-documenting
A fetch function that records the URL, the date, the licence and the checksum costs ten lines and ends this problem permanently.
Code examples
Example 1 โ fingerprint the layer
import geopandas as gpd, re
FINGERPRINTS = [
(re.compile(r"^(osm_id|osm_way_id)$", re.I), "OpenStreetMap derivative โ ODbL-1.0"),
(re.compile(r"^(LSOA\d\dCD|MSOA\d\dCD|OA\d\dCD)$"), "ONS / UK census โ OGL-UK-3.0"),
(re.compile(r"^UPRN$", re.I), "OS AddressBase โ proprietary licence"),
(re.compile(r"^GEOID\d*$"), "US Census TIGER โ public domain"),
(re.compile(r"^NUTS_ID$"), "Eurostat / GISCO โ see GISCO terms"),
(re.compile(r"^(SCALERANK|FEATURECLA)$"), "Natural Earth โ CC0-1.0"),
]
def fingerprint(path):
gdf = gpd.read_file(path, rows=1)
hits = [label for rx, label in FINGERPRINTS for c in gdf.columns if rx.match(c)]
return {"crs": str(gdf.crs), "columns": list(gdf.columns), "suggests": sorted(set(hits))}
A schema is a strong signal: nobody else calls a column FEATURECLA.
Example 2 โ the download wrapper that prevents the problem
import requests, hashlib, json, pathlib, datetime
def fetch(url, dest, licence, attribution, name=None, timeout=300):
dest = pathlib.Path(dest)
dest.parent.mkdir(parents=True, exist_ok=True)
r = requests.get(url, timeout=timeout)
r.raise_for_status()
dest.write_bytes(r.content)
record = {
"name": name or dest.stem,
"url": url,
"fetched": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
"sha256": hashlib.sha256(r.content).hexdigest(),
"bytes": len(r.content),
"licence": licence,
"attribution": attribution,
"etag": r.headers.get("ETag"),
"last_modified": r.headers.get("Last-Modified"),
}
pathlib.Path(str(dest) + ".source.json").write_text(json.dumps(record, indent=2))
return dest
Making licence a required positional argument is the whole design. A download that cannot state its licence does not happen.
Example 3 โ audit a project for unlicensed layers
import pathlib, json
def unlicensed(root="data"):
out = []
for p in pathlib.Path(root).rglob("*"):
if p.suffix.lower() not in {".gpkg", ".shp", ".geojson", ".tif", ".parquet", ".fgb"}:
continue
rec = pathlib.Path(str(p) + ".source.json")
meta = pathlib.Path(str(p).rsplit(".", 1)[0] + ".meta.json")
licence = None
for candidate in (rec, meta):
if candidate.exists():
licence = json.loads(candidate.read_text()).get("licence")
if licence:
break
if not licence:
out.append(str(p))
return out
missing = unlicensed()
if missing:
raise SystemExit(f"{len(missing)} layer(s) with no recorded licence:\n " +
"\n ".join(missing))
Run it in CI. The list only ever gets longer if nothing stops it.
Explanation
Why "it was on an open data portal" is not a licence
Portals host datasets under several licences, including restrictive ones, and many host third-party data whose terms differ from the portal's. The licence is a property of the dataset, and "it was public" is a statement about access, not about rights.
Why an unlicensed layer blocks publication rather than merely worrying you
Any output derived from it inherits an unknown obligation. If the layer turns out to be share-alike, your output may need relicensing; if it is non-commercial, the output may not be distributable at all; if it is proprietary, you may have no right to publish anything. Since you cannot know which, the only defensible position is to treat the output as unpublishable until the licence is resolved.
Why fingerprinting works so well
Spatial datasets carry the publisher's conventions in their schema, CRS and extent. Column names in particular are effectively signatures โ FEATURECLA is Natural Earth, LSOA21CD is ONS, osm_id is an OSM extract โ and identifying the publisher is nearly always enough to find the licence.
Why the fix is in the fetch function
Every instance of this problem starts with a download that recorded nothing. A wrapper that requires the licence and writes a .source.json beside the file makes the record automatic, and makes the absence of a record a visible anomaly rather than the norm.
Edge cases or notes
- Derived layers inherit. A clip of an unlicensed layer is unlicensed.
- A licence file in a zip may cover only part of it. Multi-source archives are common.
- Wayback Machine has the download page. Often the fastest route to the terms.
data.gov-style portals expose an API with licence fields; query it by dataset id.- Attribution in the data is not a licence. A copyright column tells you who, not what you may do.
- Internal data needs a licence too. "Ours" is not a distribution term.
- Record the licence version and the date. Terms change.
- Do not guess from similar datasets. The same publisher uses different licences for different products.
Internal links
- Open data licences explained for spatial data โ what the identifier you find means
- How to check licence compatibility before combining datasets โ the gate that surfaced the problem
- How to read the metadata already inside your spatial files โ searching inside the file
- Attribution requirements explained: OpenStreetMap, Copernicus and national data โ what to do once you know
- Spatial metadata explained: what a dataset must tell you โ the record that prevents this
- GIS data sources explained โ finding an open replacement
- How to cache downloaded GIS data in Python โ where the fetch wrapper belongs
- How to explore an unfamiliar spatial dataset in Python โ fingerprinting in more depth
FAQ
Where should I look for a dataset's licence?
Inside the file's metadata, in sidecar and README files, in the original zip, in the attributes, in the project file that recorded the source URL, and finally by asking whoever downloaded it.
Can I use a layer if I cannot find its licence?
Not for anything you publish. The output inherits an unknown obligation, and the possibilities include share-alike, non-commercial and proprietary.
How do I work out who published it?
Fingerprint the data: the CRS, the extent and especially the column names. osm_id, LSOA21CD, FEATURECLA and NUTS_ID each identify a publisher unambiguously.
Does being on an open data portal make it open?
No. Portals host datasets under many licences, including restrictive ones and third-party data with different terms.
What if nobody can tell me?
Replace the layer. Open equivalents exist for boundaries, addresses, roads and land cover, and swapping is cheaper than defending a publication built on an unknown.
How do I stop this happening again?
Wrap your downloads in a function that requires a licence argument and writes a .source.json next to the file with the URL, date, checksum and licence.