Skip to content

API Reference

The stable reference for this package is the top-level nyc_geo_toolkit namespace. This page is generated directly from that namespace so the published API docs, the package __all__, and the consumer contract stay aligned.

Underscore-prefixed modules are internal implementation details and are not part of the supported public surface.

Loading & Discovery

Reusable NYC geography resources, normalization helpers, and boundary loaders.

list_boundary_layers

list_boundary_layers() -> tuple[str, ...]

Return the names of all available boundary layers.

Returns:

Type Description
tuple[str, ...]

Layer names in catalog order (e.g. ("borough", "community_district", ...)).

Example

list_boundary_layers() ('borough', 'community_district', 'council_district', ...)

Source code in src/nyc_geo_toolkit/_loaders.py
58
59
60
61
62
63
64
65
66
67
68
def list_boundary_layers() -> tuple[str, ...]:
    """Return the names of all available boundary layers.

    Returns:
        Layer names in catalog order (e.g. ``("borough", "community_district", ...)``).

    Example:
        >>> list_boundary_layers()
        ('borough', 'community_district', 'council_district', ...)
    """
    return tuple(spec.layer for spec in BOUNDARY_LAYER_CATALOG)

list_boundary_values

list_boundary_values(
    layer: str, *, vintage: int | None = None
) -> tuple[str, ...]

Return the canonical values for a boundary layer.

Parameters:

Name Type Description Default
layer str

Boundary layer name or alias (e.g. "borough", "zip").

required
vintage int | None

Optional vintage year. Defaults to the layer's default vintage.

None

Returns:

Type Description
tuple[str, ...]

Canonical value strings for every feature in the layer.

Raises:

Type Description
ValueError

If the layer name is unsupported.

Source code in src/nyc_geo_toolkit/_loaders.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def list_boundary_values(
    layer: str,
    *,
    vintage: int | None = None,
) -> tuple[str, ...]:
    """Return the canonical values for a boundary layer.

    Args:
        layer: Boundary layer name or alias (e.g. ``"borough"``, ``"zip"``).
        vintage: Optional vintage year.  Defaults to the layer's default vintage.

    Returns:
        Canonical value strings for every feature in the layer.

    Raises:
        ValueError: If the layer name is unsupported.
    """
    normalized_layer = normalize_boundary_layer(layer)
    boundaries = load_nyc_boundaries(normalized_layer, vintage=vintage)
    return tuple(feature.geography_value for feature in boundaries.features)

list_available_vintages

list_available_vintages(
    layer: str | None = None,
) -> dict[str, tuple[int, ...]] | tuple[int, ...]

Return available vintage years for boundary layers.

Parameters:

Name Type Description Default
layer str | None

A specific layer name or alias. When None, returns a dict mapping every layer to its available vintages.

None

Returns:

Type Description
dict[str, tuple[int, ...]] | tuple[int, ...]

A tuple of vintage years when layer is given, or a dict mapping

dict[str, tuple[int, ...]] | tuple[int, ...]

layer names to vintage tuples when layer is None.

Raises:

Type Description
ValueError

If the layer name is unsupported.

Example

list_available_vintages("census_tract") (2000, 2010, 2020) list_available_vintages() {'borough': (2020,), 'census_tract': (2000, 2010, 2020), ...}

Source code in src/nyc_geo_toolkit/_loaders.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def list_available_vintages(
    layer: str | None = None,
) -> dict[str, tuple[int, ...]] | tuple[int, ...]:
    """Return available vintage years for boundary layers.

    Args:
        layer: A specific layer name or alias.  When ``None``, returns a dict
            mapping every layer to its available vintages.

    Returns:
        A tuple of vintage years when *layer* is given, or a dict mapping
        layer names to vintage tuples when *layer* is ``None``.

    Raises:
        ValueError: If the layer name is unsupported.

    Example:
        >>> list_available_vintages("census_tract")
        (2000, 2010, 2020)
        >>> list_available_vintages()
        {'borough': (2020,), 'census_tract': (2000, 2010, 2020), ...}
    """
    if layer is None:
        return dict(AVAILABLE_VINTAGES)
    normalized_layer = normalize_boundary_layer(layer)
    if normalized_layer not in AVAILABLE_VINTAGES:
        raise ValueError(f"No vintage data available for layer {layer!r}.")
    return AVAILABLE_VINTAGES[normalized_layer]

vintage_for_census_decade

vintage_for_census_decade(layer: str, decade: int) -> int

Return the actual vintage year for a layer given a census decade.

Most layers align directly with census decades (2000, 2010, 2020), but some use different years. For example, council districts are redistricted in 2013 and 2023 rather than 2010 and 2020.

Parameters:

Name Type Description Default
layer str

Boundary layer name or alias.

required
decade int

A census decade year (2000, 2010, or 2020).

required

Returns:

Type Description
int

The vintage year that corresponds to the requested decade.

Raises:

Type Description
ValueError

If the layer or decade has no mapping.

Example

vintage_for_census_decade("council_district", 2020) 2023

Source code in src/nyc_geo_toolkit/_loaders.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def vintage_for_census_decade(layer: str, decade: int) -> int:
    """Return the actual vintage year for a layer given a census decade.

    Most layers align directly with census decades (2000, 2010, 2020), but
    some use different years.  For example, council districts are redistricted
    in 2013 and 2023 rather than 2010 and 2020.

    Args:
        layer: Boundary layer name or alias.
        decade: A census decade year (``2000``, ``2010``, or ``2020``).

    Returns:
        The vintage year that corresponds to the requested decade.

    Raises:
        ValueError: If the layer or decade has no mapping.

    Example:
        >>> vintage_for_census_decade("council_district", 2020)
        2023
    """
    normalized_layer = normalize_boundary_layer(layer)
    key = (normalized_layer, decade)
    if key not in CENSUS_DECADE_VINTAGE_MAP:
        available_decades = sorted(
            d for (lyr, d) in CENSUS_DECADE_VINTAGE_MAP if lyr == normalized_layer
        )
        raise ValueError(
            f"No census decade mapping for {normalized_layer!r} and decade {decade}. "
            f"Available decades: {available_decades}."
        )
    return CENSUS_DECADE_VINTAGE_MAP[key]

load_nyc_boundaries

load_nyc_boundaries(
    layer: str = "community_district",
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection

Load packaged NYC boundary features for a given geography layer.

Returns a typed BoundaryCollection containing all features for the requested layer, optionally filtered to specific values.

Parameters:

Name Type Description Default
layer str

Boundary layer name. One of "borough", "community_district", "council_district", "neighborhood_tabulation_area", "census_tract", or "zcta". Common aliases like "zip" or "nta" are also accepted.

'community_district'
values str | tuple[str, ...] | list[str] | None

Optional filter. A single value string, or a sequence of values. Values are normalized automatically (e.g. "bk 01" becomes "BROOKLYN 01" for community districts).

None
vintage int | None

Optional vintage year. Defaults to the layer's default vintage (2020 for most layers, 2023 for council districts).

None

Returns:

Type Description
BoundaryCollection

A BoundaryCollection with matching features and vintage metadata.

Raises:

Type Description
ValueError

If the layer or vintage is unsupported, or no features match the requested values.

Example

boundaries = load_nyc_boundaries("borough", values="Manhattan") len(boundaries.features) 1 boundaries.vintage 2020

Source code in src/nyc_geo_toolkit/_loaders.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def load_nyc_boundaries(
    layer: str = "community_district",
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection:
    """Load packaged NYC boundary features for a given geography layer.

    Returns a typed ``BoundaryCollection`` containing all features for the
    requested layer, optionally filtered to specific values.

    Args:
        layer: Boundary layer name. One of ``"borough"``, ``"community_district"``,
            ``"council_district"``, ``"neighborhood_tabulation_area"``,
            ``"census_tract"``, or ``"zcta"``. Common aliases like ``"zip"`` or
            ``"nta"`` are also accepted.
        values: Optional filter. A single value string, or a sequence of values.
            Values are normalized automatically (e.g. ``"bk 01"`` becomes
            ``"BROOKLYN 01"`` for community districts).
        vintage: Optional vintage year. Defaults to the layer's default vintage
            (2020 for most layers, 2023 for council districts).

    Returns:
        A ``BoundaryCollection`` with matching features and vintage metadata.

    Raises:
        ValueError: If the layer or vintage is unsupported, or no features
            match the requested values.

    Example:
        >>> boundaries = load_nyc_boundaries("borough", values="Manhattan")
        >>> len(boundaries.features)
        1
        >>> boundaries.vintage
        2020
    """
    normalized_layer = normalize_boundary_layer(layer)
    resolved_vintage = _resolve_vintage(normalized_layer, vintage)
    boundary_collection = boundary_collection_from_geojson(
        load_boundary_payload(normalized_layer, vintage=resolved_vintage),
        vintage=resolved_vintage,
    )
    normalized_values = normalize_boundary_values(normalized_layer, values)
    if normalized_values is None:
        return boundary_collection
    requested_values = set(normalized_values)
    selected_features = tuple(
        feature
        for feature in boundary_collection.features
        if feature.geography_value in requested_values
    )
    if not selected_features:
        raise ValueError(
            "No boundaries matched the requested values for layer "
            f"{normalized_layer!r}: {normalized_values!r}."
        )
    return BoundaryCollection(
        geography=normalized_layer,
        features=selected_features,
        vintage=resolved_vintage,
    )

load_nyc_boundaries_geodataframe

load_nyc_boundaries_geodataframe(
    layer: str = "community_district",
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> gpd.GeoDataFrame

Load packaged NYC boundaries as a GeoPandas GeoDataFrame.

Requires the spatial extra (pip install nyc-geo-toolkit[spatial]).

Parameters:

Name Type Description Default
layer str

Boundary layer name or alias.

'community_district'
values str | tuple[str, ...] | list[str] | None

Optional value filter (see load_nyc_boundaries).

None
vintage int | None

Optional vintage year.

None

Returns:

Type Description
GeoDataFrame

A GeoDataFrame with geography, geography_value, and

GeoDataFrame

geometry columns, plus any additional properties from the source.

Raises:

Type Description
ImportError

If geopandas or shapely is not installed.

ValueError

If the layer, vintage, or values are unsupported.

Source code in src/nyc_geo_toolkit/_loaders.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def load_nyc_boundaries_geodataframe(
    layer: str = "community_district",
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> gpd.GeoDataFrame:
    """Load packaged NYC boundaries as a GeoPandas ``GeoDataFrame``.

    Requires the ``spatial`` extra (``pip install nyc-geo-toolkit[spatial]``).

    Args:
        layer: Boundary layer name or alias.
        values: Optional value filter (see ``load_nyc_boundaries``).
        vintage: Optional vintage year.

    Returns:
        A ``GeoDataFrame`` with ``geography``, ``geography_value``, and
        ``geometry`` columns, plus any additional properties from the source.

    Raises:
        ImportError: If geopandas or shapely is not installed.
        ValueError: If the layer, vintage, or values are unsupported.
    """
    return _boundary_collection_to_geodataframe(
        load_nyc_boundaries(layer, values=values, vintage=vintage)
    )

load_boundaries

load_boundaries(
    source: str | Path, *, vintage: int | None = None
) -> BoundaryCollection

Load boundaries from a packaged layer name or a GeoJSON file path.

When source is a recognized layer name (or alias), delegates to load_nyc_boundaries. When it is a file path, reads and parses the GeoJSON file directly.

Parameters:

Name Type Description Default
source str | Path

A layer name string (e.g. "borough") or a Path to a GeoJSON file.

required
vintage int | None

Optional vintage year, used only when source is a layer name.

None

Returns:

Type Description
BoundaryCollection

A BoundaryCollection parsed from the source.

Raises:

Type Description
ValueError

If the layer is unsupported or the file cannot be parsed.

Source code in src/nyc_geo_toolkit/_loaders.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def load_boundaries(
    source: str | Path,
    *,
    vintage: int | None = None,
) -> BoundaryCollection:
    """Load boundaries from a packaged layer name or a GeoJSON file path.

    When *source* is a recognized layer name (or alias), delegates to
    ``load_nyc_boundaries``.  When it is a file path, reads and parses
    the GeoJSON file directly.

    Args:
        source: A layer name string (e.g. ``"borough"``) or a ``Path`` to
            a GeoJSON file.
        vintage: Optional vintage year, used only when *source* is a layer name.

    Returns:
        A ``BoundaryCollection`` parsed from the source.

    Raises:
        ValueError: If the layer is unsupported or the file cannot be parsed.
    """
    if isinstance(source, Path) or Path(source).exists():
        return load_boundary_collection(source)
    try:
        return load_nyc_boundaries(str(source), vintage=vintage)
    except ValueError:
        return load_boundary_collection(source)

load_nyc_census_tracts

load_nyc_census_tracts(
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection

Load NYC census tract boundaries.

Convenience wrapper around load_nyc_boundaries("census_tract", ...).

Parameters:

Name Type Description Default
values str | tuple[str, ...] | list[str] | None

Optional tract filter (11-digit GEOIDs or 7-digit borough codes).

None
vintage int | None

Optional vintage year (available: 2000, 2010, 2020).

None

Returns:

Type Description
BoundaryCollection

A BoundaryCollection of census tract features.

Source code in src/nyc_geo_toolkit/_loaders.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
def load_nyc_census_tracts(
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection:
    """Load NYC census tract boundaries.

    Convenience wrapper around ``load_nyc_boundaries("census_tract", ...)``.

    Args:
        values: Optional tract filter (11-digit GEOIDs or 7-digit borough codes).
        vintage: Optional vintage year (available: 2000, 2010, 2020).

    Returns:
        A ``BoundaryCollection`` of census tract features.
    """
    return load_nyc_boundaries("census_tract", values=values, vintage=vintage)

load_nyc_council_districts

load_nyc_council_districts(
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection

Load NYC City Council district boundaries.

Convenience wrapper around load_nyc_boundaries("council_district", ...).

Parameters:

Name Type Description Default
values str | tuple[str, ...] | list[str] | None

Optional district number filter (e.g. "33").

None
vintage int | None

Optional vintage year (available: 2013, 2023).

None

Returns:

Type Description
BoundaryCollection

A BoundaryCollection of council district features.

Source code in src/nyc_geo_toolkit/_loaders.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def load_nyc_council_districts(
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection:
    """Load NYC City Council district boundaries.

    Convenience wrapper around ``load_nyc_boundaries("council_district", ...)``.

    Args:
        values: Optional district number filter (e.g. ``"33"``).
        vintage: Optional vintage year (available: 2013, 2023).

    Returns:
        A ``BoundaryCollection`` of council district features.
    """
    return load_nyc_boundaries("council_district", values=values, vintage=vintage)

load_nyc_neighborhood_tabulation_areas

load_nyc_neighborhood_tabulation_areas(
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection

Load NYC Neighborhood Tabulation Area boundaries.

Convenience wrapper around load_nyc_boundaries("neighborhood_tabulation_area", ...).

Parameters:

Name Type Description Default
values str | tuple[str, ...] | list[str] | None

Optional NTA code filter (e.g. "BK0101").

None
vintage int | None

Optional vintage year (available: 2010, 2020).

None

Returns:

Type Description
BoundaryCollection

A BoundaryCollection of NTA features.

Source code in src/nyc_geo_toolkit/_loaders.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def load_nyc_neighborhood_tabulation_areas(
    *,
    values: str | tuple[str, ...] | list[str] | None = None,
    vintage: int | None = None,
) -> BoundaryCollection:
    """Load NYC Neighborhood Tabulation Area boundaries.

    Convenience wrapper around
    ``load_nyc_boundaries("neighborhood_tabulation_area", ...)``.

    Args:
        values: Optional NTA code filter (e.g. ``"BK0101"``).
        vintage: Optional vintage year (available: 2010, 2020).

    Returns:
        A ``BoundaryCollection`` of NTA features.
    """
    return load_nyc_boundaries(
        "neighborhood_tabulation_area", values=values, vintage=vintage
    )

describe_layer

describe_layer(
    layer: str, *, vintage: int | None = None
) -> BoundaryLayerSpec

Return metadata about a boundary layer including provenance.

Parameters:

Name Type Description Default
layer str

Boundary layer name or alias.

required
vintage int | None

Optional vintage year. Defaults to the layer's default vintage.

None

Returns:

Type Description
BoundaryLayerSpec

A BoundaryLayerSpec with source name, URL, vintage, and feature count.

Raises:

Type Description
ValueError

If the layer or vintage is unsupported.

Example

spec = describe_layer("census_tract", vintage=2010) spec.source_name 'US Census Bureau TIGER/Line'

Source code in src/nyc_geo_toolkit/_loaders.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def describe_layer(
    layer: str,
    *,
    vintage: int | None = None,
) -> BoundaryLayerSpec:
    """Return metadata about a boundary layer including provenance.

    Args:
        layer: Boundary layer name or alias.
        vintage: Optional vintage year. Defaults to the layer's default vintage.

    Returns:
        A ``BoundaryLayerSpec`` with source name, URL, vintage, and feature count.

    Raises:
        ValueError: If the layer or vintage is unsupported.

    Example:
        >>> spec = describe_layer("census_tract", vintage=2010)
        >>> spec.source_name
        'US Census Bureau TIGER/Line'
    """
    normalized_layer = normalize_boundary_layer(layer)
    resolved_vintage = _resolve_vintage(normalized_layer, vintage)
    key = (normalized_layer, resolved_vintage)
    if key not in VINTAGE_LOOKUP:
        available = AVAILABLE_VINTAGES.get(normalized_layer, ())
        raise ValueError(
            f"No {normalized_layer!r} boundary data for vintage {resolved_vintage}. "
            f"Available vintages: {sorted(available)}."
        )
    return VINTAGE_LOOKUP[key]

Normalization

Reusable NYC geography resources, normalization helpers, and boundary loaders.

normalize_borough_name

normalize_borough_name(value: str) -> str

Normalize a borough name to its canonical uppercase form.

Accepts common aliases and abbreviations (e.g. "bk""BROOKLYN", "mn""MANHATTAN").

Parameters:

Name Type Description Default
value str

A borough name, abbreviation, or alias.

required

Returns:

Type Description
str

Canonical uppercase borough name (e.g. "BROOKLYN").

Raises:

Type Description
ValueError

If the value is empty or not a recognized borough.

Source code in src/nyc_geo_toolkit/_normalize.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def normalize_borough_name(value: str) -> str:
    """Normalize a borough name to its canonical uppercase form.

    Accepts common aliases and abbreviations (e.g. ``"bk"`` → ``"BROOKLYN"``,
    ``"mn"`` → ``"MANHATTAN"``).

    Args:
        value: A borough name, abbreviation, or alias.

    Returns:
        Canonical uppercase borough name (e.g. ``"BROOKLYN"``).

    Raises:
        ValueError: If the value is empty or not a recognized borough.
    """
    normalized = _normalize_space(value)
    if not normalized:
        raise ValueError("Borough values must not be empty.")
    borough = _BOROUGH_ALIASES.get(normalized.casefold(), normalized.upper())
    if borough not in SUPPORTED_BOROUGHS:
        raise ValueError(
            f"Unsupported borough name. Expected one of {SUPPORTED_BOROUGHS}, got {value!r}."
        )
    return borough

normalize_boundary_layer

normalize_boundary_layer(layer: str) -> str

Normalize a boundary layer name to its canonical form.

Accepts common aliases such as "zip""zcta", "nta""neighborhood_tabulation_area", and "community_board""community_district".

Parameters:

Name Type Description Default
layer str

A layer name or alias string.

required

Returns:

Type Description
str

Canonical layer name (e.g. "zcta", "census_tract").

Raises:

Type Description
ValueError

If the layer is not recognized.

Source code in src/nyc_geo_toolkit/_normalize.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def normalize_boundary_layer(layer: str) -> str:
    """Normalize a boundary layer name to its canonical form.

    Accepts common aliases such as ``"zip"`` → ``"zcta"``,
    ``"nta"`` → ``"neighborhood_tabulation_area"``, and
    ``"community_board"`` → ``"community_district"``.

    Args:
        layer: A layer name or alias string.

    Returns:
        Canonical layer name (e.g. ``"zcta"``, ``"census_tract"``).

    Raises:
        ValueError: If the layer is not recognized.
    """
    normalized = _normalize_space(layer).casefold().replace(" ", "_")
    canonical = _LAYER_ALIASES.get(normalized)
    if canonical is None:
        supported = ", ".join(
            sorted(
                {
                    "borough",
                    "community_district",
                    "council_district",
                    "neighborhood_tabulation_area",
                    "zcta",
                    "census_tract",
                }
            )
        )
        raise ValueError(
            f"Unsupported boundary layer. Expected one of {supported}, got {layer!r}."
        )
    return canonical

normalize_boundary_value

normalize_boundary_value(layer: str, value: str) -> str

Normalize a single boundary value for a given layer.

Each layer has its own normalization rules. For example, community district values like "bk 01" become "BROOKLYN 01", and census tract values accept both 7-digit borough codes and 11-digit GEOIDs.

Parameters:

Name Type Description Default
layer str

Boundary layer name or alias.

required
value str

A raw boundary value string.

required

Returns:

Type Description
str

The canonical normalized value.

Raises:

Type Description
ValueError

If the value cannot be parsed for the given layer.

Source code in src/nyc_geo_toolkit/_normalize.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def normalize_boundary_value(layer: str, value: str) -> str:
    """Normalize a single boundary value for a given layer.

    Each layer has its own normalization rules.  For example, community
    district values like ``"bk 01"`` become ``"BROOKLYN 01"``, and
    census tract values accept both 7-digit borough codes and 11-digit
    GEOIDs.

    Args:
        layer: Boundary layer name or alias.
        value: A raw boundary value string.

    Returns:
        The canonical normalized value.

    Raises:
        ValueError: If the value cannot be parsed for the given layer.
    """
    normalized_layer = normalize_boundary_layer(layer)
    normalized_value = _normalize_space(value)
    if not normalized_value:
        raise ValueError("Boundary values must not be empty.")
    if normalized_layer == "borough":
        return normalize_borough_name(normalized_value)
    if normalized_layer == "community_district":
        upper_value = (
            normalized_value.upper()
            .replace("COMMUNITY DISTRICT", " ")
            .replace("COMMUNITY BOARD", " ")
            .replace("DISTRICT", " ")
            .replace("BOARD", " ")
        )
        upper_value = _normalize_space(upper_value)
        match = _COMMUNITY_DISTRICT_TOKEN_RE.fullmatch(upper_value)
        if match is None:
            raise ValueError(
                "Community district values must look like 'BROOKLYN 01', "
                "'01 BROOKLYN', or a borough alias plus a district number. "
                f"Got {value!r}."
            )
        borough_token = match.group("borough") or match.group("borough_last")
        number_token = match.group("number") or match.group("number_first")
        if borough_token is None or number_token is None:
            raise ValueError(
                "Community district values must include both a borough and "
                f"a district number. Got {value!r}."
            )
        district_number = int(number_token)
        if not 1 <= district_number <= 18:
            raise ValueError(
                "Community district numbers must fall between 1 and 18. "
                f"Got {district_number!r} from {value!r}."
            )
        return f"{normalize_borough_name(borough_token)} {district_number:02d}"
    if normalized_layer == "zcta":
        matches = _ZCTA_RE.findall(normalized_value)
        if len(matches) != 1:
            raise ValueError(
                f"ZCTA values must contain exactly one 5-digit ZIP code. Got {value!r}."
            )
        return str(matches[0])
    if normalized_layer == "council_district":
        matches = _COUNCIL_DISTRICT_RE.findall(normalized_value)
        if len(matches) != 1:
            raise ValueError(
                "Council district values must contain exactly one district "
                f"number. Got {value!r}."
            )
        district_number = int(matches[0])
        if not 1 <= district_number <= 51:
            raise ValueError(
                "Council district numbers must fall between 1 and 51. "
                f"Got {district_number!r} from {value!r}."
            )
        return f"{district_number:02d}"
    if normalized_layer == "neighborhood_tabulation_area":
        compact_value = re.sub(r"[^A-Z0-9]", "", normalized_value.upper())
        match = _NTA_CODE_RE.fullmatch(compact_value)
        if match is None:
            raise ValueError(
                "Neighborhood tabulation area values must look like 'BK0101'. "
                f"Got {value!r}."
            )
        return match.group(1)
    tract_code = re.sub(r"[^0-9]", "", normalized_value)
    if len(tract_code) not in {7, 11}:
        raise ValueError(
            "Census tract values must contain exactly one 7-digit borough "
            f"tract code or 11-digit GEOID. Got {value!r}."
        )
    if len(tract_code) == 11:
        return tract_code
    borough_code = tract_code[0]
    county_fips = _COUNTY_FIPS_BY_BOROUGH_CODE.get(borough_code)
    if county_fips is None:
        raise ValueError(
            "7-digit census tract values must start with a valid borough "
            f"code 1-5. Got {value!r}."
        )
    return f"36{county_fips}{tract_code[1:]}"

normalize_boundary_values

normalize_boundary_values(
    layer: str, values: str | Iterable[str] | None
) -> tuple[str, ...] | None

Normalize one or more boundary values for a layer.

Accepts a single string, an iterable of strings, or None. Each value is passed through normalize_boundary_value. Duplicates are removed while preserving order.

Parameters:

Name Type Description Default
layer str

Boundary layer name or alias.

required
values str | Iterable[str] | None

A single value, iterable of values, or None.

required

Returns:

Type Description
tuple[str, ...] | None

A deduplicated tuple of normalized values, or None if the

tuple[str, ...] | None

input is None or empty.

Source code in src/nyc_geo_toolkit/_normalize.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def normalize_boundary_values(
    layer: str, values: str | Iterable[str] | None
) -> tuple[str, ...] | None:
    """Normalize one or more boundary values for a layer.

    Accepts a single string, an iterable of strings, or ``None``.
    Each value is passed through ``normalize_boundary_value``.
    Duplicates are removed while preserving order.

    Args:
        layer: Boundary layer name or alias.
        values: A single value, iterable of values, or ``None``.

    Returns:
        A deduplicated tuple of normalized values, or ``None`` if the
        input is ``None`` or empty.
    """
    if values is None:
        return None
    raw_values = (values,) if isinstance(values, str) else tuple(values)
    if not raw_values:
        return None
    normalized_values = [
        normalize_boundary_value(layer, value)
        for value in raw_values
        if _normalize_space(value)
    ]
    if not normalized_values:
        return None
    return tuple(dict.fromkeys(normalized_values))

Models

Reusable NYC geography resources, normalization helpers, and boundary loaders.

BoundaryCollection dataclass

A collection of boundary features for one geography type and vintage.

Attributes:

Name Type Description
geography str

Canonical layer name shared by all features.

features tuple[BoundaryFeature, ...]

Tuple of BoundaryFeature objects.

vintage int

Year the boundary data represents.

Source code in src/nyc_geo_toolkit/_models.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@dataclass(frozen=True)
class BoundaryCollection:
    """A collection of boundary features for one geography type and vintage.

    Attributes:
        geography: Canonical layer name shared by all features.
        features: Tuple of ``BoundaryFeature`` objects.
        vintage: Year the boundary data represents.
    """

    geography: str
    features: tuple[BoundaryFeature, ...]
    vintage: int = 2020

    def __post_init__(self) -> None:
        normalized_geography = normalize_boundary_layer(self.geography)
        if not self.features:
            raise ValueError("features must not be empty.")
        if normalized_geography not in SUPPORTED_BOUNDARY_GEOGRAPHIES:
            raise ValueError(f"Unsupported boundary geography {self.geography!r}.")
        for feature in self.features:
            if feature.geography != normalized_geography:
                raise ValueError(
                    "All boundary features in a collection must share the same geography."
                )
        object.__setattr__(self, "geography", normalized_geography)

BoundaryFeature dataclass

A single boundary feature with canonical geography metadata.

Attributes:

Name Type Description
geography str

Canonical layer name (e.g. "borough").

geography_value str

Canonical identifier for this feature (e.g. "MANHATTAN", "BROOKLYN 01", "10001").

geometry dict[str, Any]

GeoJSON geometry object (Polygon or MultiPolygon).

properties dict[str, Any]

Additional properties from the source data, excluding geography and geography_value which are promoted to top-level fields.

Source code in src/nyc_geo_toolkit/_models.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@dataclass(frozen=True)
class BoundaryFeature:
    """A single boundary feature with canonical geography metadata.

    Attributes:
        geography: Canonical layer name (e.g. ``"borough"``).
        geography_value: Canonical identifier for this feature
            (e.g. ``"MANHATTAN"``, ``"BROOKLYN 01"``, ``"10001"``).
        geometry: GeoJSON geometry object (``Polygon`` or ``MultiPolygon``).
        properties: Additional properties from the source data, excluding
            ``geography`` and ``geography_value`` which are promoted to
            top-level fields.
    """

    geography: str
    geography_value: str
    geometry: dict[str, Any]
    properties: dict[str, Any]

    def __post_init__(self) -> None:
        normalized_geography = normalize_boundary_layer(self.geography)
        normalized_value = _normalize_space(self.geography_value)
        if not normalized_value:
            raise ValueError("geography_value must not be empty.")
        if not isinstance(self.geometry, dict):
            raise ValueError("geometry must be a GeoJSON object.")
        if not isinstance(self.properties, dict):
            raise ValueError("properties must be a mapping.")
        object.__setattr__(self, "geography", normalized_geography)
        object.__setattr__(self, "geography_value", normalized_value)

BoundaryLayerSpec dataclass

Metadata describing one packaged NYC boundary layer.

Attributes:

Name Type Description
layer str

Canonical layer name (e.g. "census_tract").

display_name str

Human-readable label for the layer.

resource_path str

Package-relative path to the GeoJSON file.

vintage int

Year this boundary data represents (e.g. 2020, 2010).

source_name str

Name of the data source organization.

source_url str

URL to the source data page.

feature_count int

Expected number of features in the layer.

Source code in src/nyc_geo_toolkit/_models.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@dataclass(frozen=True)
class BoundaryLayerSpec:
    """Metadata describing one packaged NYC boundary layer.

    Attributes:
        layer: Canonical layer name (e.g. ``"census_tract"``).
        display_name: Human-readable label for the layer.
        resource_path: Package-relative path to the GeoJSON file.
        vintage: Year this boundary data represents (e.g. ``2020``, ``2010``).
        source_name: Name of the data source organization.
        source_url: URL to the source data page.
        feature_count: Expected number of features in the layer.
    """

    layer: str
    display_name: str
    resource_path: str
    vintage: int = 2020
    source_name: str = ""
    source_url: str = ""
    feature_count: int = 0

Conversion

Reusable NYC geography resources, normalization helpers, and boundary loaders.

boundaries_to_geojson

boundaries_to_geojson(
    boundaries: BoundaryCollection,
) -> dict[str, object]

Convert a BoundaryCollection to a GeoJSON FeatureCollection dict.

Each feature includes geography and geography_value in its properties alongside any additional source properties.

Parameters:

Name Type Description Default
boundaries BoundaryCollection

A typed boundary collection.

required

Returns:

Type Description
dict[str, object]

A GeoJSON-compatible dict with type and features keys.

Source code in src/nyc_geo_toolkit/_conversions.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def boundaries_to_geojson(boundaries: BoundaryCollection) -> dict[str, object]:
    """Convert a ``BoundaryCollection`` to a GeoJSON ``FeatureCollection`` dict.

    Each feature includes ``geography`` and ``geography_value`` in its
    properties alongside any additional source properties.

    Args:
        boundaries: A typed boundary collection.

    Returns:
        A GeoJSON-compatible dict with ``type`` and ``features`` keys.
    """
    return {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "properties": {
                    "geography": feature.geography,
                    "geography_value": feature.geography_value,
                    **feature.properties,
                },
                "geometry": feature.geometry,
            }
            for feature in boundaries.features
        ],
    }

boundaries_to_dataframe

boundaries_to_dataframe(
    boundaries: BoundaryCollection,
) -> pd.DataFrame

Convert a BoundaryCollection to a pandas DataFrame.

Requires the dataframes extra (pip install nyc-geo-toolkit[dataframes]).

Columns include geography, geography_value, geometry (as a raw GeoJSON dict), and any additional source properties.

Parameters:

Name Type Description Default
boundaries BoundaryCollection

A typed boundary collection.

required

Returns:

Type Description
DataFrame

A DataFrame with one row per feature.

Raises:

Type Description
ImportError

If pandas is not installed.

Source code in src/nyc_geo_toolkit/_conversions.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def boundaries_to_dataframe(boundaries: BoundaryCollection) -> pd.DataFrame:
    """Convert a ``BoundaryCollection`` to a pandas ``DataFrame``.

    Requires the ``dataframes`` extra
    (``pip install nyc-geo-toolkit[dataframes]``).

    Columns include ``geography``, ``geography_value``, ``geometry``
    (as a raw GeoJSON dict), and any additional source properties.

    Args:
        boundaries: A typed boundary collection.

    Returns:
        A ``DataFrame`` with one row per feature.

    Raises:
        ImportError: If pandas is not installed.
    """
    pd = _require_pandas()
    return pd.DataFrame.from_records(
        [
            {
                "geography": feature.geography,
                "geography_value": feature.geography_value,
                **feature.properties,
                "geometry": feature.geometry,
            }
            for feature in boundaries.features
        ]
    )

Geodesy

Reusable NYC geography resources, normalization helpers, and boundary loaders.

haversine_distance_meters

haversine_distance_meters(
    latitude_a: float,
    longitude_a: float,
    latitude_b: float,
    longitude_b: float,
) -> float

Return the great-circle distance between two WGS84 points in meters.

Uses the haversine formula. No external dependencies.

Parameters:

Name Type Description Default
latitude_a float

Latitude of the first point (decimal degrees).

required
longitude_a float

Longitude of the first point (decimal degrees).

required
latitude_b float

Latitude of the second point (decimal degrees).

required
longitude_b float

Longitude of the second point (decimal degrees).

required

Returns:

Type Description
float

Distance in meters.

Source code in src/nyc_geo_toolkit/_geodesy.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def haversine_distance_meters(
    latitude_a: float,
    longitude_a: float,
    latitude_b: float,
    longitude_b: float,
) -> float:
    """Return the great-circle distance between two WGS84 points in meters.

    Uses the haversine formula.  No external dependencies.

    Args:
        latitude_a: Latitude of the first point (decimal degrees).
        longitude_a: Longitude of the first point (decimal degrees).
        latitude_b: Latitude of the second point (decimal degrees).
        longitude_b: Longitude of the second point (decimal degrees).

    Returns:
        Distance in meters.
    """

    lat1 = radians(latitude_a)
    lon1 = radians(longitude_a)
    lat2 = radians(latitude_b)
    lon2 = radians(longitude_b)

    delta_lat = lat2 - lat1
    delta_lon = lon2 - lon1
    haversine_term = (
        sin(delta_lat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(delta_lon / 2) ** 2
    )
    return 2 * EARTH_RADIUS_METERS * asin(sqrt(haversine_term))

walk_radius_meters

walk_radius_meters(
    minutes: float,
    *,
    meters_per_minute: float = DEFAULT_WALKING_METERS_PER_MINUTE,
) -> float

Convert a walking-time threshold into an approximate radius in meters.

Parameters:

Name Type Description Default
minutes float

Walking time in minutes (must be positive).

required
meters_per_minute float

Walking speed. Defaults to 80 m/min (~3 mph).

DEFAULT_WALKING_METERS_PER_MINUTE

Returns:

Type Description
float

Approximate radius in meters.

Raises:

Type Description
ValueError

If minutes or meters_per_minute is not positive.

Source code in src/nyc_geo_toolkit/_geodesy.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def walk_radius_meters(
    minutes: float, *, meters_per_minute: float = DEFAULT_WALKING_METERS_PER_MINUTE
) -> float:
    """Convert a walking-time threshold into an approximate radius in meters.

    Args:
        minutes: Walking time in minutes (must be positive).
        meters_per_minute: Walking speed. Defaults to 80 m/min (~3 mph).

    Returns:
        Approximate radius in meters.

    Raises:
        ValueError: If *minutes* or *meters_per_minute* is not positive.
    """

    if minutes <= 0:
        raise ValueError(_POSITIVE_MINUTES_MESSAGE)
    if meters_per_minute <= 0:
        raise ValueError("Walking speed must be positive.")
    return float(minutes) * meters_per_minute

build_circle_polygon

build_circle_polygon(
    latitude: float,
    longitude: float,
    radius_meters: float,
    *,
    sides: int = 24,
) -> tuple[tuple[float, float], ...]

Return a WGS84 polygon approximating a circle around a point.

Coordinates are returned as (longitude, latitude) pairs following the GeoJSON convention. The polygon is closed (first point == last).

Parameters:

Name Type Description Default
latitude float

Center latitude (decimal degrees).

required
longitude float

Center longitude (decimal degrees).

required
radius_meters float

Circle radius in meters (must be positive).

required
sides int

Number of polygon sides (minimum 8).

24

Returns:

Type Description
tuple[tuple[float, float], ...]

A tuple of (lon, lat) coordinate pairs.

Raises:

Type Description
ValueError

If radius_meters is not positive or sides < 8.

Source code in src/nyc_geo_toolkit/_geodesy.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def build_circle_polygon(
    latitude: float,
    longitude: float,
    radius_meters: float,
    *,
    sides: int = 24,
) -> tuple[tuple[float, float], ...]:
    """Return a WGS84 polygon approximating a circle around a point.

    Coordinates are returned as ``(longitude, latitude)`` pairs following
    the GeoJSON convention.  The polygon is closed (first point == last).

    Args:
        latitude: Center latitude (decimal degrees).
        longitude: Center longitude (decimal degrees).
        radius_meters: Circle radius in meters (must be positive).
        sides: Number of polygon sides (minimum 8).

    Returns:
        A tuple of ``(lon, lat)`` coordinate pairs.

    Raises:
        ValueError: If *radius_meters* is not positive or *sides* < 8.
    """

    if radius_meters <= 0:
        raise ValueError(_POSITIVE_RADIUS_MESSAGE)
    if sides < 8:
        raise ValueError(_MINIMUM_SIDES_MESSAGE)

    latitude_radians = radians(latitude)
    angular_distance = radius_meters / EARTH_RADIUS_METERS
    points: list[tuple[float, float]] = []

    for index in range(sides):
        bearing = 2 * pi * (index / sides)
        lat = asin(
            sin(latitude_radians) * cos(angular_distance)
            + cos(latitude_radians) * sin(angular_distance) * cos(bearing)
        )
        lon = radians(longitude) + atan2(
            sin(bearing) * sin(angular_distance) * cos(latitude_radians),
            cos(angular_distance) - sin(latitude_radians) * sin(lat),
        )
        points.append((round(degrees(lon), 6), round(degrees(lat), 6)))

    points.append(points[0])
    return tuple(points)

Spatial Helpers

Reusable NYC geography resources, normalization helpers, and boundary loaders.

centroids_from_boundaries

centroids_from_boundaries(
    boundaries: BoundaryCollection,
    *,
    representative: bool = False,
) -> BoundaryCollection

Compute per-feature centroids as a Point BoundaryCollection.

For every polygon or multi-polygon feature in boundaries, returns a GeoJSON Point geometry at either the geometric centroid (default) or the shapely representative_point (guaranteed to fall inside the polygon, useful for non-convex shapes like community districts with concave shorelines). Feature identity — geography, geography_value, and properties — round-trips verbatim, so the returned collection can be fed back through :func:boundaries_to_geojson or :func:boundaries_to_dataframe.

Requires the spatial extra (pip install nyc-geo-toolkit[spatial]).

Parameters:

Name Type Description Default
boundaries BoundaryCollection

A BoundaryCollection of polygon / multipolygon features.

required
representative bool

When True, use shapely.Geometry.representative_point (always inside the polygon). When False (default), use the geometric centroid, which may fall outside for non-convex shapes.

False

Returns:

Type Description
BoundaryCollection

A new BoundaryCollection where each feature's geometry is a

BoundaryCollection

GeoJSON Point. The collection's geography and vintage are

BoundaryCollection

preserved.

Raises:

Type Description
ImportError

If shapely is not installed.

ValueError

If boundaries is empty (enforced by BoundaryCollection's own invariant) or contains a feature whose geometry is empty after centroid projection.

Example

from nyc_geo_toolkit import ( ... centroids_from_boundaries, ... load_nyc_boundaries, ... ) cds = load_nyc_boundaries("community_district") points = centroids_from_boundaries(cds) points.features[0].geometry["type"] 'Point'

Source code in src/nyc_geo_toolkit/_ops.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def centroids_from_boundaries(
    boundaries: BoundaryCollection,
    *,
    representative: bool = False,
) -> BoundaryCollection:
    """Compute per-feature centroids as a Point ``BoundaryCollection``.

    For every polygon or multi-polygon feature in ``boundaries``, returns a
    GeoJSON ``Point`` geometry at either the geometric centroid (default) or
    the shapely ``representative_point`` (guaranteed to fall inside the
    polygon, useful for non-convex shapes like community districts with
    concave shorelines). Feature identity — ``geography``, ``geography_value``,
    and ``properties`` — round-trips verbatim, so the returned collection
    can be fed back through :func:`boundaries_to_geojson` or
    :func:`boundaries_to_dataframe`.

    Requires the ``spatial`` extra (``pip install nyc-geo-toolkit[spatial]``).

    Args:
        boundaries: A ``BoundaryCollection`` of polygon / multipolygon features.
        representative: When ``True``, use ``shapely.Geometry.representative_point``
            (always inside the polygon). When ``False`` (default), use the
            geometric centroid, which may fall outside for non-convex shapes.

    Returns:
        A new ``BoundaryCollection`` where each feature's ``geometry`` is a
        GeoJSON ``Point``. The collection's ``geography`` and ``vintage`` are
        preserved.

    Raises:
        ImportError: If shapely is not installed.
        ValueError: If ``boundaries`` is empty (enforced by
            ``BoundaryCollection``'s own invariant) or contains a feature
            whose geometry is empty after centroid projection.

    Example:
        >>> from nyc_geo_toolkit import (
        ...     centroids_from_boundaries,
        ...     load_nyc_boundaries,
        ... )
        >>> cds = load_nyc_boundaries("community_district")
        >>> points = centroids_from_boundaries(cds)
        >>> points.features[0].geometry["type"]
        'Point'
    """
    shapely_geometry = _require_shapely()
    point_features: list[BoundaryFeature] = []
    for feature in boundaries.features:
        geometry = shapely_geometry.shape(feature.geometry)
        point = geometry.representative_point() if representative else geometry.centroid
        if point.is_empty:
            raise ValueError(
                f"Centroid for feature {feature.geography_value!r} is empty; "
                "the source geometry may be degenerate."
            )
        point_features.append(
            BoundaryFeature(
                geography=feature.geography,
                geography_value=feature.geography_value,
                geometry=shapely_geometry.mapping(point),
                properties=dict(feature.properties),
            )
        )
    return BoundaryCollection(
        geography=boundaries.geography,
        features=tuple(point_features),
        vintage=boundaries.vintage,
    )

clip_boundaries_to_bbox

clip_boundaries_to_bbox(
    boundaries: BoundaryCollection,
    *,
    min_longitude: float,
    min_latitude: float,
    max_longitude: float,
    max_latitude: float,
) -> BoundaryCollection

Clip boundary features to an axis-aligned bounding box.

Features that do not intersect the bounding box are dropped. Features that partially overlap are clipped to the box. Requires the spatial extra (pip install nyc-geo-toolkit[spatial]).

Parameters:

Name Type Description Default
boundaries BoundaryCollection

A BoundaryCollection to clip.

required
min_longitude float

Western edge (WGS84 degrees).

required
min_latitude float

Southern edge (WGS84 degrees).

required
max_longitude float

Eastern edge (WGS84 degrees).

required
max_latitude float

Northern edge (WGS84 degrees).

required

Returns:

Type Description
BoundaryCollection

A new BoundaryCollection containing only the clipped features.

Raises:

Type Description
ImportError

If shapely is not installed.

ValueError

If the bounding box is degenerate or no features intersect.

Source code in src/nyc_geo_toolkit/_ops.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def clip_boundaries_to_bbox(
    boundaries: BoundaryCollection,
    *,
    min_longitude: float,
    min_latitude: float,
    max_longitude: float,
    max_latitude: float,
) -> BoundaryCollection:
    """Clip boundary features to an axis-aligned bounding box.

    Features that do not intersect the bounding box are dropped.
    Features that partially overlap are clipped to the box.
    Requires the ``spatial`` extra (``pip install nyc-geo-toolkit[spatial]``).

    Args:
        boundaries: A ``BoundaryCollection`` to clip.
        min_longitude: Western edge (WGS84 degrees).
        min_latitude: Southern edge (WGS84 degrees).
        max_longitude: Eastern edge (WGS84 degrees).
        max_latitude: Northern edge (WGS84 degrees).

    Returns:
        A new ``BoundaryCollection`` containing only the clipped features.

    Raises:
        ImportError: If shapely is not installed.
        ValueError: If the bounding box is degenerate or no features intersect.
    """
    if min_longitude >= max_longitude or min_latitude >= max_latitude:
        raise ValueError("Bounding boxes must satisfy min < max on both axes.")
    shapely_geometry = _require_shapely()
    clip_box = shapely_geometry.box(
        min_longitude,
        min_latitude,
        max_longitude,
        max_latitude,
    )
    clipped_features: list[BoundaryFeature] = []
    for feature in boundaries.features:
        geometry = shapely_geometry.shape(feature.geometry)
        clipped_geometry = geometry.intersection(clip_box)
        if clipped_geometry.is_empty:
            continue
        clipped_features.append(
            BoundaryFeature(
                geography=feature.geography,
                geography_value=feature.geography_value,
                geometry=shapely_geometry.mapping(clipped_geometry),
                properties=dict(feature.properties),
            )
        )
    if not clipped_features:
        raise ValueError("No boundaries intersect the requested bounding box.")
    return BoundaryCollection(
        geography=boundaries.geography,
        features=tuple(clipped_features),
        vintage=boundaries.vintage,
    )

add_osm_basemap

add_osm_basemap(
    axes: Any, *, provider: Any | None = None
) -> None

Add a basemap to a matplotlib axes in Web Mercator projection.

Defaults to CartoDB Positron tiles. Requires the spatial extra (pip install nyc-geo-toolkit[spatial]).

Parameters:

Name Type Description Default
axes Any

A matplotlib Axes object (must already be in EPSG:3857).

required
provider Any | None

An optional contextily tile provider. Defaults to CartoDB.Positron.

None

Raises:

Type Description
ImportError

If contextily is not installed.

Source code in src/nyc_geo_toolkit/_basemap.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def add_osm_basemap(axes: Any, *, provider: Any | None = None) -> None:
    """Add a basemap to a matplotlib axes in Web Mercator projection.

    Defaults to CartoDB Positron tiles.  Requires the ``spatial`` extra
    (``pip install nyc-geo-toolkit[spatial]``).

    Args:
        axes: A matplotlib ``Axes`` object (must already be in EPSG:3857).
        provider: An optional contextily tile provider.  Defaults to
            ``CartoDB.Positron``.

    Raises:
        ImportError: If contextily is not installed.
    """
    ctx = import_module("contextily")
    source = provider if provider is not None else ctx.providers.CartoDB.Positron
    ctx.add_basemap(axes, source=source, attribution_size=6)

to_web_mercator

to_web_mercator(geodataframe: Any) -> Any

Return a copy of a GeoDataFrame reprojected to EPSG:3857 (Web Mercator).

Parameters:

Name Type Description Default
geodataframe Any

A geopandas GeoDataFrame in any CRS.

required

Returns:

Type Description
Any

A new GeoDataFrame in EPSG:3857.

Source code in src/nyc_geo_toolkit/_basemap.py
28
29
30
31
32
33
34
35
36
37
def to_web_mercator(geodataframe: Any) -> Any:
    """Return a copy of a GeoDataFrame reprojected to EPSG:3857 (Web Mercator).

    Args:
        geodataframe: A geopandas ``GeoDataFrame`` in any CRS.

    Returns:
        A new ``GeoDataFrame`` in EPSG:3857.
    """
    return geodataframe.to_crs(epsg=3857)

bbox_around

bbox_around(
    point: tuple[float, float], meters: float
) -> tuple[float, float, float, float]

Return a WGS84 bounding box around a point.

Computes the axis-aligned envelope of a circle with radius meters in Web Mercator, then reprojects to EPSG:4326.

Requires the spatial extra (pip install nyc-geo-toolkit[spatial]).

Parameters:

Name Type Description Default
point tuple[float, float]

(longitude, latitude) in decimal degrees.

required
meters float

Buffer radius in real-world meters (must be positive).

required

Returns:

Name Type Description
float

(min_longitude, min_latitude, max_longitude, max_latitude) in

EPSG float

4326.

Raises:

Type Description
ImportError

If geopandas or shapely is not installed.

ValueError

If meters is not positive.

Source code in src/nyc_geo_toolkit/_basemap.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def bbox_around(
    point: tuple[float, float], meters: float
) -> tuple[float, float, float, float]:
    """Return a WGS84 bounding box around a point.

    Computes the axis-aligned envelope of a circle with radius *meters*
    in Web Mercator, then reprojects to EPSG:4326.

    Requires the ``spatial`` extra
    (``pip install nyc-geo-toolkit[spatial]``).

    Args:
        point: ``(longitude, latitude)`` in decimal degrees.
        meters: Buffer radius in real-world meters (must be positive).

    Returns:
        ``(min_longitude, min_latitude, max_longitude, max_latitude)`` in
        EPSG:4326.

    Raises:
        ImportError: If geopandas or shapely is not installed.
        ValueError: If *meters* is not positive.
    """
    if meters <= 0:
        raise ValueError("meters must be positive.")

    gpd = import_module("geopandas")
    shapely_geometry = import_module("shapely.geometry")
    point_cls = shapely_geometry.Point
    box_cls = shapely_geometry.box

    gdf = gpd.GeoDataFrame(geometry=[point_cls(point[0], point[1])], crs="EPSG:4326")
    g3857 = gdf.to_crs(epsg=3857)
    buffered = g3857.buffer(meters)
    minx, miny, maxx, maxy = buffered.total_bounds
    envelope = gpd.GeoDataFrame(
        geometry=[box_cls(minx, miny, maxx, maxy)], crs="EPSG:3857"
    )
    out = envelope.to_crs(epsg=4326)
    tb = out.total_bounds
    return (float(tb[0]), float(tb[1]), float(tb[2]), float(tb[3]))

Constants

Reusable NYC geography resources, normalization helpers, and boundary loaders.

DEFAULT_VINTAGE module-attribute

DEFAULT_VINTAGE: Final[int] = 2020

SUPPORTED_BOROUGHS module-attribute

SUPPORTED_BOROUGHS: Final[tuple[BoroughName, ...]] = (
    BOROUGH_BRONX,
    BOROUGH_BROOKLYN,
    BOROUGH_MANHATTAN,
    BOROUGH_QUEENS,
    BOROUGH_STATEN_ISLAND,
)

SUPPORTED_BOUNDARY_GEOGRAPHIES module-attribute

SUPPORTED_BOUNDARY_GEOGRAPHIES: Final[tuple[str, ...]] = (
    "borough",
    "community_district",
    "council_district",
    "neighborhood_tabulation_area",
    "census_tract",
    "zcta",
)

BOROUGH_BRONX module-attribute

BOROUGH_BRONX: Final[BoroughName] = 'BRONX'

BOROUGH_BROOKLYN module-attribute

BOROUGH_BROOKLYN: Final[BoroughName] = 'BROOKLYN'

BOROUGH_MANHATTAN module-attribute

BOROUGH_MANHATTAN: Final[BoroughName] = 'MANHATTAN'

BOROUGH_QUEENS module-attribute

BOROUGH_QUEENS: Final[BoroughName] = 'QUEENS'

BOROUGH_STATEN_ISLAND module-attribute

BOROUGH_STATEN_ISLAND: Final[BoroughName] = 'STATEN ISLAND'