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.

Vertical steps through the six places a licence for a downloaded layer can be found.
Six places, cheapest first; the sixth is asking a person.

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.

Table of identifying features โ€” CRS, column names, extent โ€” against the publisher each suggests.
The data usually identifies its own publisher, which is most of the way to its licence.

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.

Flow showing a fetch wrapper that requires a licence argument, writes the file and a source record, and is audited in CI.
Making the licence a required argument is the whole design.

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.

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.