Open Data License Selection & Comparison
Choosing an open license for a spatial dataset is a decision with long-lived engineering consequences: it determines whether downstream users may combine your layer with commercial products, whether they must open-source their derivatives, and whether your attribution string survives three transformations deep into someone else’s pipeline. Publishing under the wrong instrument is expensive to reverse — once a dataset circulates under CC BY-SA, you cannot retroactively relax the share-alike obligation for existing copies. This guide sits within Geospatial Data Licensing & Compliance Fundamentals and gives GIS data managers, open-data maintainers, and automation engineers a concrete framework — and runnable Python — for selecting among ODbL, CC BY 4.0, CC BY-SA 4.0, CC0, PDDL, and OGL, and for verifying that a chosen license is compatible with every layer it will be combined with.
Prerequisites
Before wiring license selection into a publishing pipeline, establish these baselines:
- Python 3.11+ with
geopandas≥ 0.14,fiona≥ 1.9,pandas≥ 2.0, andjsonschema≥ 4.19. These handle vector inventory reading and schema validation of license metadata. - An SPDX vocabulary baseline: agree on the exact short-form identifiers your organisation will emit —
ODbL-1.0,CC-BY-4.0,CC-BY-SA-4.0,CC0-1.0,PDDL-1.0,OGL-UK-3.0— and treat any value outside that set as an error requiring review. - A catalog field for license: your STAC items, GeoPackage metadata, or sidecar JSON must expose a single canonical
licensefield. The ISO 19115 metadata template generation page covers where this field lives in structured records. - Environment variables for any portal push step:
LICENSE_POLICY_PATHpointing at your rules file, andSPATIAL_INVENTORY_DIRpointing at the dataset tree to scan. - Baseline knowledge of the difference between copyright and database rights — the two legal regimes that CC and ODbL respectively target — because that distinction drives most selection decisions.
Install the working stack:
python -m pip install \
"geopandas>=0.14,<1.1" \
"fiona>=1.9" \
"pandas>=2.0" \
"jsonschema>=4.19"
Concept & Spec Reference
An open license for spatial data operates on one of two legal foundations. Copyright protects original creative expression — the cartographic styling of a map, the selection and arrangement in an atlas, the pixels of an aerial image. Database rights (an EU sui generis regime, with functional equivalents applied through contract elsewhere) protect the substantial investment in obtaining, verifying, and presenting a collection of facts, independent of any creativity. A road centreline table has thin copyright but strong database-right value; a hillshade rendering is the reverse. Selecting a license means matching the instrument to the regime that actually protects your asset.
The six licenses in scope differ across four axes that determine every downstream obligation: whether attribution is required, whether share-alike propagates to derivatives, whether commercial reuse is permitted, and whether the license explicitly addresses database rights.
| License | SPDX ID | Attribution? | Share-alike? | Commercial use? | Database rights addressed? | Best fit |
|---|---|---|---|---|---|---|
| Public Domain Dedication (CC0) | CC0-1.0 |
No | No | Yes | Waived | Reference layers, statistical geographies |
| Open Data Commons PDDL | PDDL-1.0 |
No | No | Yes | Waived | Public-domain data in the ODbL toolchain |
| Creative Commons Attribution | CC-BY-4.0 |
Yes | No | Yes | Yes (4.0 covers sui generis) | Imagery, basemaps, single-source layers |
| Creative Commons Attribution-ShareAlike | CC-BY-SA-4.0 |
Yes | Yes (any adaptation) | Yes | Yes (4.0 covers sui generis) | Community-maintained cartographic works |
| Open Database License | ODbL-1.0 |
Yes | Yes (distributed derivative DBs) | Yes | Yes (primary target) | Vector feature databases, gazetteers |
| Open Government Licence (UK) | OGL-UK-3.0 |
Yes | No | Yes | Yes (via contract) | UK public-sector datasets |
Two subtleties trip up practitioners. First, the CC 4.0 suite explicitly extends its conditions to database rights, which earlier 3.0 versions did not — so a modern CC BY 4.0 layer does carry a database-rights waiver where applicable. Second, ODbL’s share-alike is narrower than CC BY-SA’s: ODbL triggers share-alike only when a derivative database is publicly distributed, leaving internal analytics untouched, whereas CC BY-SA triggers on any distributed adaptation regardless of whether it is a database. The ODbL vs CC BY-SA for vector datasets guide works through that distinction in full for feature layers.
Implementation Walkthrough
Step 1 — Encode the licenses as a rules dictionary
Rationale: a selection or compatibility engine is only as trustworthy as the facts it encodes. Rather than scatter license logic across if branches, define one authoritative rules table keyed by SPDX identifier. Every axis from the spec table becomes a queryable attribute.
from dataclasses import dataclass, field
from typing import Dict, List
@dataclass(frozen=True)
class LicenseRule:
spdx: str
requires_attribution: bool
share_alike: bool # does the license impose a copyleft obligation
commercial_ok: bool
covers_database_rights: bool
# SPDX IDs this license's obligations remain satisfiable alongside
compatible_with: frozenset = field(default_factory=frozenset)
# The authoritative rules table. Compatibility here means "the combined
# product can satisfy both licenses' obligations simultaneously".
LICENSE_RULES: Dict[str, LicenseRule] = {
"CC0-1.0": LicenseRule("CC0-1.0", False, False, True, True,
frozenset({"CC0-1.0", "PDDL-1.0", "CC-BY-4.0",
"CC-BY-SA-4.0", "ODbL-1.0", "OGL-UK-3.0"})),
"PDDL-1.0": LicenseRule("PDDL-1.0", False, False, True, True,
frozenset({"CC0-1.0", "PDDL-1.0", "CC-BY-4.0",
"CC-BY-SA-4.0", "ODbL-1.0", "OGL-UK-3.0"})),
"CC-BY-4.0": LicenseRule("CC-BY-4.0", True, False, True, True,
frozenset({"CC0-1.0", "PDDL-1.0", "CC-BY-4.0",
"CC-BY-SA-4.0", "ODbL-1.0", "OGL-UK-3.0"})),
"CC-BY-SA-4.0": LicenseRule("CC-BY-SA-4.0", True, True, True, True,
frozenset({"CC0-1.0", "PDDL-1.0", "CC-BY-4.0",
"CC-BY-SA-4.0"})),
"ODbL-1.0": LicenseRule("ODbL-1.0", True, True, True, True,
frozenset({"CC0-1.0", "PDDL-1.0", "CC-BY-4.0",
"ODbL-1.0"})),
"OGL-UK-3.0": LicenseRule("OGL-UK-3.0", True, False, True, True,
frozenset({"CC0-1.0", "PDDL-1.0", "CC-BY-4.0",
"OGL-UK-3.0"})),
}
Step 2 — Turn intent into a license recommendation
Rationale: selection should be reproducible, not a matter of memory. Capturing the maintainer’s intent as a small set of booleans lets the same inputs always yield the same recommendation, and makes the decision auditable months later.
from typing import Optional
def recommend_license(
is_database_of_facts: bool,
want_share_alike: bool,
want_attribution: bool,
uk_public_sector: bool = False,
) -> str:
"""Return the SPDX id best matching a maintainer's stated intent."""
if uk_public_sector:
return "OGL-UK-3.0"
if not want_attribution and not want_share_alike:
# No conditions desired -> public-domain dedication.
return "PDDL-1.0" if is_database_of_facts else "CC0-1.0"
if is_database_of_facts:
# Database rights are the protectable asset.
return "ODbL-1.0" if want_share_alike else "CC-BY-4.0"
# Creative work: copyright is the protectable asset.
return "CC-BY-SA-4.0" if want_share_alike else "CC-BY-4.0"
def explain_recommendation(spdx: str) -> Optional[dict]:
"""Return the obligation profile for a recommended license."""
rule = LICENSE_RULES.get(spdx)
if rule is None:
return None
return {
"license": rule.spdx,
"must_attribute": rule.requires_attribution,
"share_alike": rule.share_alike,
"commercial_allowed": rule.commercial_ok,
}
Step 3 — Check compatibility before combining layers
Rationale: most open-license failures are not selection errors but combination errors — a maintainer joins an ODbL road network to a CC BY-SA landcover layer and cannot lawfully release the result under either. A compatibility checker run at the join stage catches this before publication.
from itertools import combinations
from typing import List, Tuple
def check_pairwise_compatibility(spdx_ids: List[str]) -> List[Tuple[str, str]]:
"""Return the list of incompatible license pairs among the inputs."""
conflicts: List[Tuple[str, str]] = []
for a, b in combinations(sorted(set(spdx_ids)), 2):
rule_a = LICENSE_RULES.get(a)
rule_b = LICENSE_RULES.get(b)
if rule_a is None or rule_b is None:
conflicts.append((a, b)) # unknown license -> treat as conflict
continue
if b not in rule_a.compatible_with or a not in rule_b.compatible_with:
conflicts.append((a, b))
return conflicts
def resolve_output_license(spdx_ids: List[str]) -> Optional[str]:
"""
Determine the license the combined product must carry. A share-alike
input forces its own license onto the output. Returns None if the
inputs are mutually incompatible.
"""
if check_pairwise_compatibility(spdx_ids):
return None
share_alike = [s for s in set(spdx_ids)
if LICENSE_RULES[s].share_alike]
if len(share_alike) == 1:
return share_alike[0] # the copyleft term dominates
if len(share_alike) > 1:
return None # competing copyleft terms -> no lawful output
# No share-alike input: the most demanding attribution license governs.
attributed = [s for s in set(spdx_ids)
if LICENSE_RULES[s].requires_attribution]
return attributed[0] if attributed else "CC0-1.0"
Validation & CI Integration
License selection is only durable if it is verified on every publish. Validate that each dataset’s recorded license field is a known SPDX identifier, and that any multi-source product carries a lawful output license.
import jsonschema
LICENSE_FIELD_SCHEMA = {
"type": "object",
"required": ["license"],
"properties": {
"license": {"type": "string", "enum": sorted(LICENSE_RULES.keys())}
},
}
def validate_license_field(record: dict) -> list:
"""Return a list of validation errors for a catalog record."""
errors: list = []
try:
jsonschema.validate(instance=record, schema=LICENSE_FIELD_SCHEMA)
except jsonschema.ValidationError as exc:
errors.append(exc.message)
return errors
Wire the same check into a pre-commit hook so no dataset lands without a validated license:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: open-license-check
name: Validate open license selection for spatial datasets
language: python
entry: python scripts/check_open_license.py
files: '\.(gpkg|shp|geojson)$'
additional_dependencies: ["jsonschema>=4.19"]
Lock the logic in with a pytest regression suite so future edits to the rules table cannot silently loosen an obligation:
# tests/test_license_selection.py
from scripts.license_rules import (
recommend_license, resolve_output_license, check_pairwise_compatibility,
)
def test_database_share_alike_yields_odbl():
assert recommend_license(True, True, True) == "ODbL-1.0"
def test_creative_share_alike_yields_cc_by_sa():
assert recommend_license(False, True, True) == "CC-BY-SA-4.0"
def test_odbl_and_cc_by_sa_are_incompatible():
assert check_pairwise_compatibility(["ODbL-1.0", "CC-BY-SA-4.0"])
def test_cc_by_absorbed_into_share_alike():
assert resolve_output_license(["CC-BY-4.0", "CC-BY-SA-4.0"]) == "CC-BY-SA-4.0"
def test_public_domain_inputs_yield_cc0():
assert resolve_output_license(["CC0-1.0", "PDDL-1.0"]) == "CC0-1.0"
This mirrors the merge-gating approach used across the site’s spatial data schema linting in CI patterns: a red test blocks the PR.
Derivative & Lineage Management
The moment you clip, join, or enrich one licensed layer with another, the output’s license is no longer a free choice — it is computed from the inputs. Three principles govern that computation:
- Share-alike propagates. A single CC BY-SA or ODbL input imposes its copyleft term on any publicly distributed derivative. The
resolve_output_licensefunction above encodes this: one share-alike input dominates the output license, and two competing share-alike terms produce no lawful output at all. This is exactly the mechanics detailed in choosing a license for derived spatial products. - Attribution accumulates, it does not merge. Combining five CC BY layers does not collapse into one credit line — every source’s required attribution must appear in the derivative’s metadata and any human-visible surface. Record every contributing source’s SPDX ID and attribution string in the derivative’s catalog entry so credit generation stays automatic.
- The scope of the trigger matters. ODbL’s share-alike fires only on a publicly distributed derivative database; an internal analytical raster derived from ODbL vectors and never redistributed does not trigger it. Recording the deployment context (
internalvspublic) alongside the lineage lets the pipeline apply the obligation only where it actually bites.
Persist the lineage — source fingerprints, their licenses, the computed output license, and the deployment context — in the same catalog record as the derivative. That chain is what converts a compatibility decision into defensible, auditable evidence when a data provider or downstream consumer questions how a layer was licensed.
Pitfalls & Resolution Table
| Pitfall | Root Cause | Resolution Strategy |
|---|---|---|
| A CC BY 3.0 layer’s database contents reused without attribution, assumed safe | CC 3.0 did not extend its conditions to database rights, so teams assume no obligation — but many jurisdictions grant sui generis rights the 3.0 license never waived | Treat pre-4.0 CC data conservatively; require attribution and, where possible, obtain the source under a 4.0 relicense or ODbL before reuse |
| ODbL road layer joined to CC BY-SA landcover, output un-publishable | Both licenses impose incompatible share-alike terms on the combined work | Detect the conflict at join time with check_pairwise_compatibility; source one layer under a permissive license (CC BY, CC0) instead |
| Output of a CC BY-SA adaptation released under plain CC BY | Maintainer forgot that share-alike propagates to every adaptation, not just verbatim copies | Compute the output license with resolve_output_license rather than choosing it manually; gate publication on the result |
| Public-domain layer (CC0) shipped with a mandatory attribution notice | Confusing professional courtesy (“please cite”) with a legal obligation, then propagating it as if binding | Model CC0/PDDL with requires_attribution=False; keep citation requests in documentation, not in the enforced license field |
SPDX identifier ODbL recorded instead of ODbL-1.0 |
Hand-typed license strings drift from the canonical SPDX short form | Validate the license field against the enum in LICENSE_FIELD_SCHEMA in CI; reject any non-canonical value |
| OGL-UK data combined with ODbL data assumed automatically compatible | OGL permits attribution-only reuse but does not neutralise ODbL’s database share-alike when the combined product is a derivative database | Model OGL as compatible only with permissive and attribution-only licenses; require review before combining with any copyleft source |
| Internal analytics blocked because ODbL share-alike was applied to a non-distributed derivative | The pipeline treated all derivatives as public without tracking deployment context | Record deployment_context; apply ODbL share-alike only when the derivative database is publicly distributed |
Related
- ODbL vs CC BY-SA for Vector Datasets — a head-to-head verdict for feature layers, with an inventory-tagging script
- Choosing a License for Derived Spatial Products — how clip/join/enrich operations compute the output license from mixed sources
- Commercial EULA Compliance Tracking — the parallel discipline for proprietary datasets that fall outside the SPDX open-license space
- Geospatial Risk Scoring Frameworks — quantify the exposure that an unknown or incompatible license introduces into an inventory
- Geospatial Data Licensing & Compliance Fundamentals — the parent guide to licensing obligations, risk surfaces, and metadata requirements