Creative Commons Licensing for GIS Datasets
Creative Commons licensing is the operational standard for open geospatial data distribution, yet its implementation exposes unique technical challenges absent from traditional media licensing: spatial metadata schemas lack a universal license field, derivative tracking must survive format conversions and coordinate reprojections, and automated compliance pipelines must parse binary spatial files rather than plain text. GIS data managers, open-source maintainers, and government technology teams working within the broader framework of Geospatial Data Licensing & Compliance Fundamentals must embed licenses at the format level, validate them programmatically, and propagate attribution obligations through every transformation step to maintain defensible compliance.
Prerequisites & Environment Configuration
Before implementing automated CC licensing workflows, establish the following baseline:
- Python 3.9+,
geopandas>=0.13,pyproj>=3.4,lxml>=4.9,rasterio>=1.3, andgdal(viaosgeo). These libraries handle spatial I/O, CRS validation, and XML/JSON metadata generation. - License selection policy: internal documentation mapping use cases to specific CC identifiers (
CC0-1.0,CC-BY-4.0,CC-BY-SA-4.0,CC-BY-NC-4.0). Non-Commercial and No-Derivatives clauses create integration friction and are generally discouraged for foundational spatial layers. - Metadata schema familiarity: working knowledge of ISO 19115-1, Dublin Core, and GeoJSON community conventions. Spatial datasets require license metadata in both human-readable sidecar files and machine-readable attributes.
- Version control + provenance tracking: Git LFS or DVC for large spatial files, with a commit strategy that records license changes alongside data updates. Immutable provenance is essential when downstream consumers rely on specific license versions.
Concept & Spec Reference
Creative Commons licenses are modular: each variant activates a specific set of permissions and restrictions. In geospatial contexts, the four licenses in active use differ primarily in their effect on spatial joins, commercial API deployments, and catalogue redistribution.
| License (SPDX) | Attribution | Commercial | Derivatives | Typical GIS Use Case |
|---|---|---|---|---|
CC0-1.0 |
Not required | Permitted | Permitted | Administrative boundaries, reference basemaps, census geometries |
CC-BY-4.0 |
Required | Permitted | Permitted | Government spatial data portals, academic research layers |
CC-BY-SA-4.0 |
Required | Permitted | Must re-license same | OpenStreetMap-derived layers; viral across joined datasets |
CC-BY-NC-4.0 |
Required | Prohibited | Permitted | Academic/municipal data not intended for commercial platforms |
Key geospatial-specific points:
- Database rights: in EU jurisdictions, spatial datasets may attract sui generis database rights independent of copyright. CC licenses address these rights since version 4.0 — earlier versions do not, creating gaps for European open-data portals.
- NC clause and enterprise GIS: the CC-BY-NC restriction on commercial use conflicts directly with commercial EULA compliance tracking requirements for enterprise GIS stacks that charge users for hosted map services.
- ShareAlike and OSM derivatives: any dataset derived from OpenStreetMap (licensed
ODbL-1.0) carries separate ShareAlike obligations; do not confuseODbL-1.0withCC-BY-SA-4.0— they are not interchangeable.
Normative references (non-hyperlinked per site rules): ISO 19115-1:2014/Amd 1:2018; SPDX License List 3.x; CC License 4.0 International legal text.
Implementation Walkthrough
Step 1 — Inject license into GeoJSON
GeoJSON has no mandated license field, but community conventions treat a root-level "license" key as canonical. Add it before publication:
import json
from pathlib import Path
CC_BY_URI = "https://creativecommons.org/licenses/by/4.0/"
def inject_geojson_license(path: Path, license_uri: str = CC_BY_URI) -> None:
"""Add or replace the root-level license key in a GeoJSON file."""
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
data["license"] = license_uri
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
Rationale: root-level injection is preferred over per-feature properties because license terms apply to the whole dataset, not individual features.
Step 2 — Embed license in GeoPackage metadata table
GeoPackage’s gpkg_metadata table stores ISO 19115 or Dublin Core XML records linked to layers via gpkg_metadata_reference. A minimal Dublin Core block satisfies discovery requirements:
import sqlite3
from pathlib import Path
DUBLIN_CORE_TMPL = """<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://purl.org/dc/elements/1.1/">
<identifier>{dataset_id}</identifier>
<rights>{license_uri}</rights>
<license>{license_uri}</license>
</metadata>"""
def embed_gpkg_license(gpkg_path: Path, dataset_id: str, license_uri: str) -> None:
"""Insert a Dublin Core license record into gpkg_metadata."""
xml_content = DUBLIN_CORE_TMPL.format(
dataset_id=dataset_id, license_uri=license_uri
)
conn = sqlite3.connect(str(gpkg_path))
try:
conn.execute(
"INSERT OR REPLACE INTO gpkg_metadata "
"(id, md_scope, md_standard_uri, mime_type, metadata) "
"VALUES (1, 'dataset', 'http://dublincore.org/documents/dces/', "
"'text/xml', ?)",
(xml_content,)
)
conn.execute(
"INSERT OR REPLACE INTO gpkg_metadata_reference "
"(reference_scope, md_file_id) VALUES ('geopackage', 1)"
)
conn.commit()
finally:
conn.close()
Rationale: modifying gpkg_metadata is the only standards-compliant path; writing to layer attribute columns is non-standard and breaks inter-operability with QGIS and ArcGIS metadata readers.
Step 3 — Embed license in GeoTIFF via GDAL
Raster datasets require GDAL metadata tags. The ImageDescription tag and the GDAL_METADATA domain are the two injection points:
from osgeo import gdal
from pathlib import Path
def embed_geotiff_license(tif_path: Path, license_uri: str) -> None:
"""Write CC license URI into GeoTIFF GDAL metadata domain."""
ds = gdal.Open(str(tif_path), gdal.GA_Update)
if ds is None:
raise FileNotFoundError(f"GDAL could not open: {tif_path}")
ds.SetMetadataItem("LICENSE", license_uri)
ds.SetMetadataItem("LICENSE", license_uri, "GDAL_METADATA")
ds.FlushCache()
ds = None # close dataset
Rationale: FlushCache() writes metadata without re-encoding pixel data, preserving compression and georeferencing intact.
Step 4 — Preserve license across geopandas operations
geopandas does not persist custom attributes through GeoDataFrame.to_crs() or spatial overlay calls. Preserve the license by re-injecting it on every output write:
import geopandas as gpd
from pathlib import Path
def reproject_with_license(
src: Path, dst: Path, target_crs: str, license_uri: str
) -> None:
"""Reproject a vector file and restore the license field in the output."""
gdf = gpd.read_file(src)
gdf_reprojected = gdf.to_crs(target_crs)
# Re-inject as a dataset-level property in GeoJSON, or as a column for GPKG
if dst.suffix == ".geojson":
gdf_reprojected.to_file(dst, driver="GeoJSON")
inject_geojson_license(dst, license_uri)
elif dst.suffix == ".gpkg":
gdf_reprojected.to_file(dst, driver="GPKG", layer="data")
embed_gpkg_license(dst, dataset_id=dst.stem, license_uri=license_uri)
else:
raise ValueError(f"Unsupported output format: {dst.suffix}")
Rationale: making license injection the last step of every write operation is more reliable than attempting to propagate it through intermediate GeoDataFrame objects.
Validation & CI Integration
Manual metadata checks fail at scale. A robust validation pipeline parses spatial files, checks CC URIs against an allowlist, and halts publication on failure.
import json
import logging
import sqlite3
from pathlib import Path
from typing import Optional
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
ALLOWED_CC_URIS: set[str] = {
"https://creativecommons.org/publicdomain/zero/1.0/",
"https://creativecommons.org/licenses/by/4.0/",
"https://creativecommons.org/licenses/by-sa/4.0/",
"https://creativecommons.org/licenses/by-nc/4.0/",
}
def validate_geojson_license(path: Path) -> dict[str, Optional[str]]:
"""Validate the CC license field in a GeoJSON file."""
try:
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
uri = data.get("license") or data.get("properties", {}).get("license")
if not uri:
logging.warning("Missing license in %s", path.name)
return {"status": "missing", "uri": None, "file": str(path)}
if uri not in ALLOWED_CC_URIS:
logging.error("Unrecognized CC URI in %s: %s", path.name, uri)
return {"status": "invalid", "uri": uri, "file": str(path)}
return {"status": "valid", "uri": uri, "file": str(path)}
except (json.JSONDecodeError, KeyError) as exc:
logging.error("Parse error in %s: %s", path.name, exc)
return {"status": "parse_error", "uri": None, "file": str(path)}
def validate_gpkg_license(path: Path) -> dict[str, Optional[str]]:
"""Check whether any gpkg_metadata row references a valid CC URI."""
try:
conn = sqlite3.connect(str(path))
try:
cur = conn.execute(
"SELECT metadata FROM gpkg_metadata LIMIT 20"
)
rows = [r[0] or "" for r in cur.fetchall()]
finally:
conn.close()
for uri in ALLOWED_CC_URIS:
if any(uri in row for row in rows):
return {"status": "valid", "uri": uri, "file": str(path)}
return {"status": "missing", "uri": None, "file": str(path)}
except Exception as exc:
logging.error("GPKG read error in %s: %s", path.name, exc)
return {"status": "read_error", "uri": None, "file": str(path)}
def run_compliance_check(directory: Path) -> list[dict]:
"""Scan a directory tree and return per-file validation results."""
results = []
for p in directory.rglob("*"):
if p.suffix == ".geojson":
results.append(validate_geojson_license(p))
elif p.suffix == ".gpkg":
results.append(validate_gpkg_license(p))
return results
def assert_all_valid(directory: Path) -> None:
"""Raise RuntimeError if any spatial file lacks a valid CC license."""
results = run_compliance_check(directory)
failures = [r for r in results if r["status"] != "valid"]
if failures:
for f in failures:
logging.error("License failure [%s]: %s", f["status"], f["file"])
raise RuntimeError(
f"{len(failures)} file(s) failed license validation — see log above."
)
Pre-commit hook — add to .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: spatial-license-check
name: Validate CC licenses in spatial files
language: python
entry: python -c "from validate_licenses import assert_all_valid; from pathlib import Path; assert_all_valid(Path('data/'))"
pass_filenames: false
types: []
For broader spatial data schema linting in CI, combine this license check with attribute-schema validation under the same pre-commit phase so a single git commit catches both structural and license failures.
ogrinfo spot-check — verify a GeoJSON file’s metadata attributes directly:
ogrinfo -al -so layers/admin_boundaries.geojson | grep -i license
Derivative & Lineage Management
When datasets undergo spatial transformations — buffering, clipping, rasterization, or attribute joins — the original license imposes downstream obligations. CC-BY-SA requires derivative works to carry the same license, which directly conflicts with proprietary data blending.
Lineage tracking — maintain a derivation_chain array in dataset metadata to record parent URIs and transformation types:
import json
from pathlib import Path
from datetime import datetime, timezone
def append_derivation_step(
geojson_path: Path,
parent_uri: str,
transform: str,
inherited_license: str,
) -> None:
"""Record a derivation step in the dataset's metadata."""
with open(geojson_path, "r", encoding="utf-8") as fh:
data = json.load(fh)
chain = data.get("derivation_chain", [])
chain.append({
"parent": parent_uri,
"transform": transform,
"license": inherited_license,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
data["derivation_chain"] = chain
data["license"] = inherited_license # propagate
with open(geojson_path, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
Boundary isolation — use spatial indexes to keep CC-BY-SA layers physically separate from proprietary layers in the same workspace. A pre-publish script should scan for mixed-license spatial joins and halt if incompatible licenses are detected.
Attribution propagation through spatial joins — for detailed per-feature attribution tracking when joining CC-BY layers with other sources, see how to track CC-BY-SA attribution in shapefiles.
For production environments serving many layers simultaneously, integrate with automated attribution mapping workflows to centralise attribution string generation, multi-language rendering, and fallback handling for datasets with incomplete metadata.
Pitfalls & Resolution Table
| Pitfall | Root Cause | Resolution Strategy |
|---|---|---|
| License field stripped on reproject | gdf.to_crs() discards non-geometry metadata |
Always call the injection function as the last step after every write; never rely on in-memory GeoDataFrame attributes to survive disk round-trips |
| EU database rights gap | CC 3.0 and earlier do not explicitly cover sui generis database rights | Upgrade all European-origin datasets to CC 4.0 identifiers; audit any CC 3.0 layers in the inventory |
| Broken ISO 19115 XML sidecars | Invalid UTF-8 encoding or schema violations | Pre-validate XML with lxml.etree.XMLSchema before writing; enforce encoding="UTF-8" on all file handles |
| Shapefile field truncation | .dbf 10-char field name + 254-char value limit |
Store full CC URIs in .xml sidecar only; use .dbf for short codes (CC-BY-4.0) and cross-reference the sidecar in documentation |
| NC clause in commercial SaaS pipeline | Commercial platform ingests CC-BY-NC layer without licence-awareness | Implement a licence-aware API gateway that reads the license field and filters or blocks CC-BY-NC datasets for tenants operating commercially |
| ShareAlike contamination | CC-BY-SA layer joined to a non-SA proprietary layer | Run boundary-isolation checks in CI; fail the build if gpkg_metadata or GeoJSON derivation_chain shows incompatible license pairs in a merged dataset |
| ODbL/CC-BY-SA confusion | Engineers treat OSM-derived data as CC-BY-SA | Maintain a license registry mapping each dataset ID to its exact SPDX identifier; validate registry entries against the allowlist on every pipeline run |
Related
- Geospatial Data Licensing & Compliance Fundamentals — parent overview covering the full licensing and metadata compliance domain
- How to track CC-BY-SA attribution in shapefiles — per-feature attribution tracking through spatial joins and topology operations
- Automated Attribution Mapping Workflows — centralised attribution string generation and dynamic rendering for multi-layer web maps
- Commercial EULA Compliance Tracking — how proprietary dataset constraints interact with open CC layers in enterprise stacks
- Spatial Data Schema Linting in CI — combine license validation with attribute-schema checks in a single pre-commit gate