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 it has outgrown the
scope: discovery sources, provenance bundles, matchups and staging 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. The migration back into geotoolz has not happened, and the light
base install did not survive. Both are 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¶
From the repo’s own docs. You build a GeoCatalog once, from a directory of
files, a STAC search, a NASA CMR call or a hand-made list. You hand it
GeoSlices to query, and it hands back smaller catalogs (the matching rows).
When you 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, pre-catalog records that carry the query that produced them. 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_bounds,temporal_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 into
ten namespaces by workflow step (since 0.3.0), 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. To change
one, 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”.
It carries everything the next stage needs to fetch a chip without
consulting the catalog again: shape, height, width and transform are
derived from it, and to_crs() reprojects it.
Grid alignment is the one subtlety. With bounds and resolution , the pixel shape is
which silently accepts an extent that is not a whole number of pixels
((1) rounds half up). That 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. is_grid_aligned checks exactly that.
Why GeoSlice lives here¶
This decision was locked in by the scoping report, 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. geopatcher does not import geocatalog.A separate package would be over-engineering.
GeoSliceis about 330 lines.
Table 2 shows who imports it: so far, 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:
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, in memory otherwise.
engine="duckdb" or "memory" forces one. A remote DuckDB catalog reads
only the row groups a query touches. strict=True 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; 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. 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: 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; it is a queryable index.
pipekit composes operators that transform a carrier, and geocatalog finds
which files match a query. You 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:
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 move happened. 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: you open a catalog and load slices yourself, 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. That 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. The 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 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). geotoolz is at 0.10.2. What the split unblocked, as the
report hoped: catalog backends, schema migrations and new sources 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, with how each 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: did not happen (Section ??).
GeoSliceas a cross-library type: it isgeocatalog.GeoSlice, but no other library imports it yet.Independent backend work: yes. 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: 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: documented in the repo’s concepts page, and checkable with
isinstance.
Open questions¶
Which repo is canonical? The code lives in the geotoolz monorepo; 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, georeader and the raster loader move behind an extra.Where do
CatalogPipelineandCatalogReaderlive? In geotoolz, as scoped, which needs geotoolz to depend on geocatalog. Or 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; catalog-aware operators might be.Is
slices()the right bridge?CatalogDomain.slices()returns every row’s slice eagerly. 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. Nothing in the design notes picks it up; DuckDB over GeoParquet covers the scale cases so far.
Upcoming work¶
Table 11 lists the follow-ups.
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; the set-algebra and backend figures on this page are its own.
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.