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:

  1. Python 3.11+ with geopandas ≥ 0.14, fiona ≥ 1.9, pandas ≥ 2.0, and jsonschema ≥ 4.19. These handle vector inventory reading and schema validation of license metadata.
  2. 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.
  3. A catalog field for license: your STAC items, GeoPackage metadata, or sidecar JSON must expose a single canonical license field. The ISO 19115 metadata template generation page covers where this field lives in structured records.
  4. Environment variables for any portal push step: LICENSE_POLICY_PATH pointing at your rules file, and SPATIAL_INVENTORY_DIR pointing at the dataset tree to scan.
  5. 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"

Open Data License Selection Decision Tree A top-down decision tree. The first decision asks whether any rights should be retained; if not, it routes to CC0 or PDDL. If yes, the second decision asks whether the asset is a database of facts or a creative work. The database branch routes to ODbL when share-alike is wanted and to a permissive database dedication otherwise. The creative branch routes to CC BY-SA when share-alike is wanted and CC BY when only attribution is wanted. A government branch routes to OGL. Spatial dataset ready to publish openly Retain any rights? no — waive all CC0 / PDDL no conditions yes Database of facts? yes — data Want share-alike? ODbL-1.0 SA on DBs no PDDL / CC0 or CC BY no — creative Want share-alike? yes CC BY-SA 4.0 SA on adapt. CC BY 4.0 attrib. only UK public sector? use OGL-UK-3.0 (attribution)

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_license function 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 (internal vs public) 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