geocatalog
Report 6 · The standalone spatiotemporal index, as scoped and as built
Abstract¶
A catalog is an index over geospatial files. Each row holds one file’s footprint, time interval, CRS and path, so a question like “which files overlap this area between these dates?” is answered in milliseconds, without opening any file. This report scoped geocatalog as a standalone package extracted from geotoolz. It has been built and has outgrown the scope, since discovery sources, provenance bundles, matchups and staging now sit on top of the index. Most of the scoping decisions held (GeoSlice lives in the catalog, there is no pipekit dependency, and the patcher bridge is a Protocol). However, the migration back into geotoolz has not happened and the light base install did not survive, so we leave both as open questions at the end.
About this report
| Status | Built; originally a scoping proposal (“committed”) for the catalog split |
| Reading time | about 20 minutes |
| Audience | anyone using or extending the catalog, or deciding how geotoolz and xrtoolz should consume it |
| Companions | Report 2 (pipekit core); Report 3 (sister libraries); Report 7 (geopatcher); Report 9 (use cases); Report 16 (the geo data stack) |
| Design and figures | the geocatalog repo’s docs (concepts, quickstart, design notes) and figures |
| Code checked against | geotoolz-catalog 0.5.1, import name geocatalog, in the geotoolz monorepo at 088475e (the revision pinned in references/). Examples are from real runs |
Where geocatalog sits¶
The scoping report placed geocatalog beside pipekit and geopatcher, all three above georeader and below geotoolz and xrtoolz (the picture is drawn in Report 7; the stack-wide rules are in Report 16). That still holds. geocatalog owns the spatiotemporal index end to end, has no pipekit reference at all, loads geopatcher and geocloud only lazily inside the functions that need them, and consumes georeader rather than replacing it.
What is in geocatalog¶
The mental model¶
This is the picture from the repo’s own docs. We build a GeoCatalog once, from a directory of files, a STAC search, a NASA CMR call or a hand-made list. We then hand it GeoSlices to query, and it hands back smaller catalogs (the matching rows). When we want pixels, a loader takes the slice and the matching catalog and returns a GeoTensor or an xr.Dataset. At every step the geometry, time and CRS are explicit, so there is no hidden state (Figure 1).
Figure 1:Catalogs are the index; GeoSlice is the unit of work; loaders materialise it. Redrawn from the data-flow diagram in the geocatalog repo’s concepts page.
It works in three layers, in order:
Discover.
Sourceadapters yieldSourceRows, which are pre-catalog records that carry the query that produced them, whereas the local builders read filenames and metadata directly.Index. A
GeoCatalogProtocol implementation. Both backends expose the same surface:query,intersect,union,iter_rows,iter_slices,total_boundsandtemporal_extent.Materialise. Loaders take a
GeoSliceplus a catalog and return a carrier.
Source layout and public surface¶
The scoping report counted 11 files and about 3,600 lines from geotoolz’s catalog module. The current package is about 14,300 lines, organised since 0.3.0 into ten namespaces by workflow step, each a thin facade over geocatalog._src (Table 1):
Table 1:geocatalog’s public namespaces at 0.5.1. Names in italics need an extra and import lazily.
| Namespace | Public names |
|---|---|
geocatalog (root) | GeoCatalog, GeoSlice, open_catalog, query, intersect, union |
geocatalog.backends | InMemoryGeoCatalog, DuckDBGeoCatalog, CatalogRow, GeoCatalogError, CatalogMetadataError, CatalogSchemaError, CatalogClosedError |
geocatalog.build | build_raster_catalog, build_vector_catalog, build_xarray_catalog, append_files |
geocatalog.load | load_raster, aload_raster, load_raster_timeseries, load_vector, load_xarray |
geocatalog.grid | Align, GridAlignmentWarning, count_steps, is_grid_aligned, slice_to_window, window_to_slice, PIXEL_PRECISION |
geocatalog.storage | to_geoparquet, from_geoparquet, migrate_geoparquet, sort_geoparquet, StreamingParquetWriter, CatalogBundle, QueryRecord, to_stac_collection, SCHEMA_VERSION_CURRENT |
geocatalog.sources | Source, SourceRow, AuthStatus, STACSource, EarthAccessSource, CMRSource, GEESource, from_stac_items, from_stac_search, source_row_to_gdf_row |
geocatalog.matchup | matchup, MatchupRow; spatial strategies Intersects, Contains, CentroidWithin, IouAtLeast; temporal Synchronous, WithinWindow, NearestInTime |
geocatalog.staging | stage |
geocatalog.patch | CatalogDomain, field_for |
geocatalog.utils | parse_uri, ParsedURI, retry_transient_io, time helpers (to_naive_utc, to_rfc3339, …) |
There is also a geocatalog command line, with build {raster, xarray, vector}, query, stats, migrate, convert and info.
GeoSlice, the unit of work¶
GeoSlice is a frozen dataclass of four fields (bounds, interval (a closed pd.Interval), resolution and crs), plus an align mode that is excluded from equality and hashing. Because it is frozen, a slice can be cached, hashed, sent across processes, or logged and replayed, and to change one we use dataclasses.replace. The repo’s docs give the reason: “silent mutation of a query that’s been logged is the worst kind of bug”.
A slice carries everything the next stage needs to fetch a chip without consulting the catalog again. The shape, height, width and transform are derived from it, and to_crs() reprojects it.
Grid alignment is the one subtlety. The bounds are corner coordinates in the slice’s CRS, (map coordinates, not inference variables), and the resolution is . The pixel counts along each axis are then
These expressions silently accept an extent that is not a whole number of pixels ((1) rounds half up), which bites at matchup boundaries, where a chip is stacked against a label raster one pixel short. aligned_shape() is the strict version, and the align= mode chooses what construction does about a residual:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22import pandas as pd from geocatalog import GeoSlice from geocatalog.grid import is_grid_aligned iv = pd.Interval(pd.Timestamp("2024-06-01"), pd.Timestamp("2024-06-05"), closed="both") s = GeoSlice(bounds=(0.0, 0.0, 105.0, 60.0), interval=iv, resolution=(10.0, 10.0), crs="EPSG:32630") # x-extent 105 = 10.5 px s.shape # → (6, 11) rounds half up, silently s.aligned_shape() # ValueError: x-extent=105.0 is not an integer multiple of # step=10.0 (nearest is 10 -> residual -5). Use align='snap' ... snap = GeoSlice(bounds=(0.0, 0.0, 105.0, 60.0), interval=iv, resolution=(10.0, 10.0), crs="EPSG:32630", align="snap") snap.bounds # → (0.0, 0.0, 110.0, 60.0) rounds outward, keeps the origin t = GeoSlice(bounds=(5.0, 0.0, 65.0, 60.0), interval=iv, resolution=(10.0, 10.0), crs="EPSG:32630") is_grid_aligned(s, t, explain=True) # → {'aligned': False, 'crs_match': True, 'x_res_match': True, # 'y_res_match': True, 'x_origin_residual': -5.0, 'y_origin_residual': 0.0}
Grid alignment at geocatalog 0.5.1, from a real run.
The modes are "off" (the default), "warn" (a GridAlignmentWarning), "error" and "snap". Snapping extends xmax right and ymin down, so the affine origin (xmin, ymax) never moves. Two slices are on the same pixel lattice when they share a CRS and resolution and their origins agree modulo the resolution, which is exactly what is_grid_aligned checks.
Why GeoSlice lives here¶
The scoping report locked in this decision, and the code bears it out:
The catalog is the primary producer.
catalog.iter_slices(...)andCatalogDomain.slices()yieldGeoSlices, and every loader consumes one.The patcher does not use it. geopatcher’s unit is a
Patch(data plus anchor plus neighbourhood), not a bounded request, and geopatcher does not import geocatalog.A separate package would be over-engineering.
GeoSliceis about 330 lines.
Table 2 shows who imports it, which so far is only geocatalog and user code.
Table 2:Who imports GeoSlice, planned and at the pinned revisions.
| Consumer | Planned | At the pinned revisions |
|---|---|---|
| geocatalog | geocatalog._src | geocatalog._src.geoslice (exported from the root) |
| geotoolz | catalog-aware operators, samplers, CatalogPipeline | no import; geotoolz’s ruff config bans geocatalog._src |
| xrtoolz | catalog-aware xarray loaders | no import |
| geopatcher | does not import it | does not import it, as planned |
| user code | from geocatalog import GeoSlice | the same |
The row schema¶
Every backend shares one row schema (Table 3):
Table 3:The shared row schema, from the geocatalog repo’s concepts page.
| Column | Type | Meaning |
|---|---|---|
geometry | Shapely polygon | the file’s footprint, in the catalog’s CRS |
start_time, end_time (promoted to an IntervalIndex) | timestamp | the time interval, closed on both ends |
filepath | str | path or URI to the source file |
crs | str | the file’s own CRS, which may differ from the catalog’s |
Each kind of catalog adds its own columns:
xarray catalogs add
n_timestepsandtime_var;vector catalogs add
layer;STAC ingestion adds
asset_key,stac_item_idandstac_collection;bundles add
id,source,collection,assetsandprovenance.
The IntervalIndex is what makes temporal queries cheap. Combined with an R-tree on geometry (in memory) or bbox-column pushdown (DuckDB), query(slice) is two cheap index lookups, intersected. Times are held as naive UTC throughout, and footprints ingested from STAC or CMR that cross the antimeridian are split into a MultiPolygon at ±180°.
Two backends, one Protocol¶
Both backends implement GeoCatalog, and the Protocol is the contract, so anywhere code accepts a GeoCatalog either one works (isinstance checks work against it). Table 4 compares the two, and Figure 2 shows where each one wins on latency.
Table 4:The two backends, from the geocatalog repo’s concepts page.
InMemoryGeoCatalog | DuckDBGeoCatalog | |
|---|---|---|
| Install | base | the [duckdb] extra |
| Storage | a GeoDataFrame in memory | GeoParquet 1.1 on disk, S3 or Hugging Face |
| Indexing | R-tree plus IntervalIndex | GeoParquet 1.1 covering bbox column |
| Scale | up to about 10⁵ rows | 10⁶ rows and more |
| Remote URIs | read through fsspec | native, through DuckDB’s httpfs |
| Mutation | a new instance per operation | a new instance per operation (a lazy SQL relation) |
| Build | eager | streaming, in bounded memory |

Figure 2:Where each backend wins. From the geocatalog repo. The curves are illustrative, synthetic rather than measured; the shape is the point.
open_catalog(source, *, engine="auto", …) picks a backend for a GeoParquet artifact, DuckDB when the extra is installed and in memory otherwise, and engine="duckdb" or "memory" forces one. A remote DuckDB catalog reads only the row groups a query touches. With strict=True it raises CatalogMetadataError on incomplete metadata instead of inferring it.
Set algebra¶
query, intersect and union all return new catalogs and leave their inputs untouched, so they are safe to compose and to cache (Figure 3).

Figure 3:Catalog set algebra. From the geocatalog repo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33import geopandas as gpd import pandas as pd from shapely.geometry import box from geocatalog import GeoSlice, intersect, open_catalog, query, union from geocatalog.backends import InMemoryGeoCatalog from geocatalog.storage import from_geoparquet, to_geoparquet gdf = gpd.GeoDataFrame( {"filepath": ["a.tif", "b.tif"], "start_time": pd.to_datetime(["2024-06-01", "2024-06-10"]), "end_time": pd.to_datetime(["2024-06-02", "2024-06-11"])}, geometry=[box(0, 0, 100, 100), box(50, 50, 150, 150)], # overlap in space, crs="EPSG:32630", # not in time ) cat = InMemoryGeoCatalog(gdf, kind="raster") cat.total_bounds # → (0.0, 0.0, 150.0, 150.0) s = GeoSlice(bounds=(0.0, 0.0, 60.0, 60.0), interval=pd.Interval(pd.Timestamp("2024-06-01"), pd.Timestamp("2024-06-05"), closed="both"), resolution=(10.0, 10.0), crs="EPSG:32630") list(query(cat, s).gdf["filepath"]) # → ['a.tif'] b.tif misses the window a = InMemoryGeoCatalog(gdf.iloc[[0]].copy(), kind="raster") b = InMemoryGeoCatalog(gdf.iloc[[1]].copy(), kind="raster") len(intersect(a, b)) # → 0 disjoint in time len(intersect(a, b, spatial_only=True)) # → 1 the (50, 50, 100, 100) overlap len(union(a, b)) # → 2 to_geoparquet(cat, "cat.parquet") # GeoParquet 1.1, with a bbox column len(from_geoparquet("cat.parquet")) # → 2 open_catalog("cat.parquet", engine="memory") # → an InMemoryGeoCatalog
An in-memory catalog end to end at geocatalog 0.5.1, from a real run.
intersect(…, spatial_only=True) is the tool for pairing imagery with static labels, which have no temporal overlap by construction.
Why the loaders live here¶
The scoping report kept the loaders in geocatalog rather than georeader or geotoolz, and that held:
They are catalog API. The pattern is “the catalog produces slices, a loader consumes them”, and both sides ship together.
They keep georeader independent. georeader stays a reader, writer and
GeoTensorlibrary that knows nothing about catalogs.
load_raster reprojects each source into the slice’s CRS, range-reads remote files through GDAL, and has an async twin, aload_raster. load_raster_timeseries derives its time steps from the slice. The scoping report expected the loaders to be extras-gated, but at 0.5.1 only the xarray and vector loaders are lazy and the raster loader is part of the base install (Section ??).
What shipped beyond the scope¶
The scoping report described an index with builders and loaders, whereas the current package adds the pieces that turn it into a data-acquisition tool. Most of them come from the repo’s query → matchup → patch design (Table 5):
Table 5:Shipped features the scoping report did not cover.
| Feature | Where | What it does |
|---|---|---|
| Discovery sources | geocatalog.sources | STACSource, EarthAccessSource, CMRSource, GEESource behind one Source Protocol, yielding SourceRows with their query |
| Provenance bundles | geocatalog.storage.CatalogBundle | items, query records and matchups persisted as one directory (ingest, write_matchups, to_directory, from_directory) |
| Matchups | geocatalog.matchup | pair rows across catalogs with a spatial strategy (Intersects, IouAtLeast, …) and a temporal one (Synchronous, WithinWindow, NearestInTime) |
| Staging | geocatalog.staging.stage | download a catalog’s assets in parallel with retries, through geocloud’s LocalCache |
| Exact grid alignment | geocatalog.grid | align= modes, aligned_shape, is_grid_aligned (Section ??) |
| Hive partitioning and appends | to_geoparquet(partition_by=…), build.append_files | archives that grow; only new rows are written |
| Schema versions | storage.migrate_geoparquet, SCHEMA_VERSION_CURRENT (0) | forward migrations of the GeoParquet schema |
| STAC export | storage.to_stac_collection | a catalog as a STAC collection |
| Remote I/O | utils.parse_uri, utils.retry_transient_io | URI resolution through GDAL /vsi paths or geocloud’s filesystems; retries with backoff |
| CLI | geocatalog … | build, query, stats, migrate, convert, info |
Dependencies and extras¶
The scoping report wanted an index-only install with no GDAL, made of pandas, geopandas, pyproj, shapely and rasterio (for GeoSlice’s affine transforms and windows). Loaders, DuckDB and streaming would be extras. Table 6 and Table 7 set that against what shipped.
Table 6:The base install, scoped and shipped.
| Scoped base | Shipped base at 0.5.1 |
|---|---|
| pandas, geopandas, pyproj, shapely, rasterio | pandas, geopandas, pyproj, shapely, rasterio, georeader-spaceml, pyarrow, numpy, typing-extensions, loguru, cyclopts, tenacity |
Table 7:The optional extras, scoped and shipped.
| Scoped | Shipped |
|---|---|
duckdb = [duckdb] | duckdb = [duckdb>=1.1] (1.1 for bbox-column pushdown) |
raster = [georeader] | none: georeader is in the base |
xarray = [xarray, rioxarray] | xarray-raster = [xarray, rioxarray, h5netcdf, zarr] |
vector = [pyogrio] | none: pyogrio arrives with geopandas |
streaming = [pyarrow] | streaming = [duckdb]: pyarrow is in the base |
| — | cloud = [geotoolz-cloud[fsspec]], stac, earthaccess, gee, sources-all, patch = [geotoolz-patcher>=0.6], full |
No pipekit dependency. This held exactly, and it is the cleanest decision in the report. The catalog is not an Operator but a queryable index: pipekit composes operators that transform a carrier, whereas geocatalog finds which files match a query. We can open a catalog without pipekit, and a pipekit pipeline can read from a source that is not a catalog. The two meet in the consumer libraries, which is where the missing operators belong (Section ??).
The patcher bridge¶
CatalogDomain lets a geopatcher patcher walk a catalog without either package importing the other, and it shipped as designed in that respect. geopatcher’s Domain is a runtime_checkable Protocol with two members, crs and bounds. CatalogDomain has both, so it satisfies Domain structurally: isinstance(CatalogDomain(...), geopatcher.Domain) returns True at runtime, and neither package imports the other at module level.
1 2 3 4 5from geocatalog.patch import CatalogDomain dom = CatalogDomain(cat, resolution=(10.0, 10.0)) dom.bounds, dom.crs.to_epsg() # → ((0.0, 0.0, 150.0, 150.0), 32630) len(dom.slices()) # → 2 one GeoSlice per catalog row
CatalogDomain at geocatalog 0.5.1, from a real run on the catalog above.
field_for is the second half of the bridge, which the scoping report did not have. With the [patch] extra it hands a catalog to geopatcher as a RasterField (or a list of them), importing geopatcher lazily inside the call:
1 2 3 4from geocatalog.patch import field_for field = field_for(catalog, slice_, asset="visual") # → geopatcher.RasterField # geopatcher's SpatialPatcher then splits `field` into patches.
Catalog-driven iteration meeting patch-driven iteration (a sketch; needs the [patch] extra).
Migration from geotoolz¶
The scoping report planned a transitional period with three steps:
move
geotoolz.catalogandgeotoolz.types.GeoSliceinto geocatalog;have geotoolz re-export everything (including
import geocatalog as catalog) for two minor versions, then deprecate the re-exports;keep only the pipekit-shaped operators in a new
geotoolz.catalog_ops.
1 2 3 4 5 6class CatalogPipeline(Operator): """ETL driver: iterate a catalog, apply an op per scene, maybe in parallel.""" def __init__(self, catalog: GeoCatalog, op: Operator, *, n_workers=1): ... class CatalogReader(Operator): """Read one GeoSlice into a GeoTensor, inline in a Sequential."""
The consumer operators the scoping report planned for geotoolz (not built).
The first step happened and the rest did not (Table 8):
Table 8:The migration steps at the pinned revisions.
| Step | Status |
|---|---|
| Catalog code moves out of geotoolz into its own package | done: geocatalog, now geotoolz-catalog in the same monorepo |
GeoSlice becomes geocatalog.GeoSlice | done |
geotoolz re-exports the catalog (gz.open_catalog, gz.catalog, geotoolz.types.GeoSlice) | not done; geotoolz has no catalog names and no types module |
| Deprecation over two minor versions | not needed: there was no compatibility layer to deprecate |
CatalogPipeline, CatalogReader in geotoolz.catalog_ops | not built |
| xrtoolz depends on geocatalog | not done |
The effect is that geocatalog is used on its own: we open a catalog and load slices ourselves, then hand the results to geotoolz or xrtoolz operators. The scoping report’s cross-library examples still work in that form, with today’s names:
1 2 3 4 5 6 7 8 9 10 11from geocatalog import open_catalog from geocatalog.load import load_raster, load_xarray cat = open_catalog("s3://bucket/scenes.parquet") # DuckDB if installed for sl in cat.iter_slices(resolution=(60.0, 60.0)): # one GeoSlice per row gt = load_raster(cat, sl) # → GeoTensor, for geotoolz ... for sl in reanalysis.iter_slices(resolution=(0.25, 0.25)): ds = load_xarray(reanalysis, sl) # → xr.Dataset, for xrtoolz ...
One catalog feeding both domain libraries, in today’s names (a sketch).
CatalogPipeline is now assumed by three reports (2, 3 and this one) and by use cases 2 and 12, and it exists nowhere, which makes it the most important open item here.
The monorepo development model¶
The scoping report chose to develop everything in one “pipekit-ecosystem” monorepo with uv workspaces and publish separate packages, for four reasons:
cross-package changes are common early on;
GeoSlicechanges ripple through every consumer;fixtures can be shared;
one CI can run the integration matrix.
What exists is three monorepos, each a uv workspace with per-package releases (Table 9; Report 16 maps every distribution in them):
Table 9:The workspaces at the pinned revisions.
| Workspace | Packages |
|---|---|
| pipekit | pipekit, pipekit-array, pipekit-cycle, pipekit-evaluate, pipekit-experiment, pipekit-jax, pipekit-train |
| geotoolz | geotoolz, geotoolz-catalog (geocatalog), geotoolz-cloud, geotoolz-patcher, geotoolz-products |
| xrtoolz | xrtoolz, xrtoolz-core, xrtoolz-einx, xrtoolz-grad, xrtoolz-reader, xrtoolz-sklearn |
The operating pattern held inside each workspace. Packages resolve to local paths during development, and release-please cuts per-package tags (geotoolz-catalog-v0.5.1). Cross-package pins appear in published metadata, for example patch = [geotoolz-patcher>=0.6]. The catalog landed in the geotoolz workspace, next to the patcher and cloud I/O it works with most, rather than beside pipekit. geostack itself is where all three workspaces meet (as submodules in references/).
Effort and timing¶
The scoping report estimated a week of focused work, done in one coordinated v1.0 release alongside the pipekit and geopatcher extractions, so that users would absorb one breaking change rather than three. The record is in Table 10.
Table 10:geocatalog’s history.
| When | What |
|---|---|
| May 2026 | standalone geocatalog repo, through 0.0.2 (25 May): the index and both backends, builders, loaders, sources, bundles, matchups, staging |
| late May – June 2026 | Hive partitioning, fsspec URIs, retries, STAC builders, obstore, exact grid alignment (0.0.3, 9 June) |
| 13 July 2026 | geocatalog and geopatcher merge into the geotoolz monorepo (geotoolz-catalog 0.1.0) |
| 8 October 2026 | 0.3.0: organised by workflow step; flat re-exports removed (breaking) |
| 12 October 2026 | 0.5.1, the pinned revision |
There was no single v1.0. The extraction was gradual, with breaking changes absorbed per package (geocatalog 0.3.0, geopatcher’s matching reorganisation), and geotoolz is at 0.10.2. The split unblocked what the report hoped, which is that catalog backends, schema migrations and new sources are released on their own cycle. There have been twelve geotoolz-catalog releases since July (0.1.0 to 0.5.1), each tagged on its own.
Trade-offs¶
The scoping report’s list is below, with how each item turned out.
What got better:
Discoverability, partly. The repo and the import name are
geocatalog, but the distribution from the monorepo isgeotoolz-catalog, which is not what someone searching for a catalog would find (Section ??).A minimal install for narrow use: this did not happen (Section ??).
GeoSliceas a cross-library type: it isgeocatalog.GeoSlice, but no other library imports it yet.Independent backend work: this worked, as DuckDB parity, GeoParquet 1.1 pushdown and schema migrations all shipped in catalog releases.
What got worse, and the mitigations:
One more package to release: release-please makes it one tag.
Version pinning between packages: this is largely moot, because geotoolz does not depend on geocatalog yet.
Documentation has to live somewhere: it lives in the standalone repo (mkdocs, with the figures and the Lake Tahoe notebook), separate from the code since July.
The Protocol bridge is implicit: it is documented in the repo’s concepts page, and we can check it with
isinstance.
Open questions¶
Which repo is canonical? The code lives in the geotoolz monorepo, whereas the docs, figures and notebook live in the standalone repo, frozen at 0.0.3. Either the docs move into the monorepo (and the standalone repo is archived with a pointer), or the standalone repo becomes the docs site for
geotoolz-catalog.geocatalogorgeotoolz-catalogon PyPI? Resolved:geotoolz-catalog(Report 16 §F).Should there be an index-only install? The scoped base had no georeader, and no rasterio beyond what
GeoSliceneeds. If the narrow use case matters, we would move georeader and the raster loader behind an extra.Where do
CatalogPipelineandCatalogReaderlive? They could live in geotoolz, as scoped, which needs geotoolz to depend on geocatalog. Alternatively they could live in an optionalgeocatalog[pipekit]module, which breaks the no-pipekit rule only behind an extra. Reports 2 and 3 both depend on the answer.Should geotoolz and xrtoolz adopt geocatalog at all? Today the catalog is used standalone and the domain libraries never see a
GeoSlice. The scoped re-exports may not be worth a dependency, whereas catalog-aware operators might be.Is
slices()the right bridge?CatalogDomain.slices()returns every row’s slice eagerly, whereas the scopedrows_for(anchor)was lazy and query-driven, which matters for large catalogs walked by a patcher.A third backend? The scoping report floated PostGIS, but nothing in the design notes picks it up, and DuckDB over GeoParquet covers the scale cases so far.
Upcoming work¶
The follow-ups are listed in Table 11.
Table 11:Follow-ups from checking the scoping report against the repo and the code.
| Follow-up | Where | Why |
|---|---|---|
| Fix the data-flow figure: the arrowheads cover the box labels | geocatalog repo, docs/assets/make_diagrams.py | this page redraws it instead |
| Regenerate the Lake Tahoe NDVI composite, which renders as an empty plot | geocatalog repo, docs/assets/lake_tahoe_ndvi_composite.png | the quickstart’s result figure shows no data |
| Replace the illustrative backend curves with measured ones | geocatalog, from tests/bench/ | the figure says “synthetic, not measured” |
| Update the docs’ examples to the namespaced 0.3.0 API | geocatalog docs | gc.load_raster, gc.to_geoparquet no longer exist |
Point the concepts page at geopatcher.SpatialPatcher | geocatalog docs | it names geotoolz.patch.SpatialPatcher |
Remove the [vector] extra from docstrings | geocatalog._src.formats.vector | the extra is not defined |
| Index geocatalog’s namespaces in the capability index | geostack scripts/capabilities.py | only the root’s six names are listed (Report 3) |
Build CatalogPipeline / CatalogReader once question 4 is settled | geotoolz or geocatalog | use cases 2 and 12; Reports 2, 3 and 6 |
References¶
Sources¶
The geocatalog repo: concepts, quickstart, recipes and the design notes on exact grid alignment and query → matchup → patch; we took the set-algebra and backend figures on this page from there.
geotoolz-catalog0.5.1 in the geotoolz monorepo, and its changelog.
Citations to be added: GeoParquet 1.1, DuckDB, STAC, and NASA CMR, as entries in references.bib.