@@ -49,6 +49,23 @@ def _as_list(value: Any) -> Any:
4949 return [value ]
5050
5151
52+ def _point_origins (points : Any ) -> list [dict [str , float ]]:
53+ """Normalise a batch of point origins to ``[{"lon": .., "lat": ..}, ...]``.
54+
55+ Each item is either a ``(lat, lon)`` pair — matching the ``(lat, lon)``
56+ argument order of the single-point calls — or a ``{"lat": .., "lon": ..}``
57+ mapping.
58+ """
59+ out : list [dict [str , float ]] = []
60+ for point in points :
61+ if isinstance (point , dict ):
62+ out .append ({"lon" : point ["lon" ], "lat" : point ["lat" ]})
63+ else :
64+ lat , lon = point
65+ out .append ({"lon" : lon , "lat" : lat })
66+ return out
67+
68+
5269def _int (value : str | None ) -> int | None :
5370 return int (value ) if value is not None else None
5471
@@ -329,39 +346,78 @@ def places(self, q: str, *, limit: int | None = None, output: str | None = None)
329346 # -- origin block / point (metered) -------------------------------------
330347
331348 def block_summary (
332- self , geoid : str , * , mode = None , type = None ,
349+ self , geoid , * , mode = None , type = None ,
333350 if_none_match : str | None = None , output : str | None = None ,
334351 ):
335352 """Fastest travel time to each destination category from a census block,
336353 by mode. `geoid` is a 15-digit block GEOID; `mode`/`type` are optional
337354 filters (scalar or list). Supports `if_none_match` (ETag/304). A DataFrame
338- (the origin GEOID broadcast to a `geoid` column), or a raw `Reply`."""
355+ (the origin GEOID broadcast to a `geoid` column), or a raw `Reply`.
356+
357+ Pass a **list of GEOIDs** to query many blocks in one call (one request,
358+ one rate-limit tick): the frame is the flat, `geoid`-tagged union of every
359+ origin's rows, and per-origin `errors` / `truncated` / `truncated_reason`
360+ ride on `df.attrs`. `if_none_match` applies to the single-block form only.
361+ """
362+ if isinstance (geoid , (list , tuple )):
363+ reply = self ._request ("POST" , "/v1/blocks/summary" , json = _clean (
364+ {"origins" : list (geoid ), "mode" : _as_list (mode ),
365+ "type" : _as_list (type )}))
366+ return self ._deliver (reply , geometry = False , key = "results" ,
367+ output = output )
339368 return self ._deliver (
340369 self ._get (f"/v1/blocks/{ geoid } /summary" ,
341370 {"mode" : mode , "type" : type }, if_none_match = if_none_match ),
342371 geometry = False , key = "results" , output = output ,
343372 )
344373
345374 def block_pois (
346- self , geoid : str , * , mode = None , type = None , dest_id = None ,
375+ self , geoid , * , mode = None , type = None , dest_id = None ,
347376 max_minutes = None , limit = None , output : str | None = None ,
348377 ):
349378 """Every nearby POI and its travel time from a block, one row per (POI,
350379 mode). A GeoDataFrame of points (a plain DataFrame under
351- `output="tabular"`, a `Paginator` under `output="raw"`)."""
380+ `output="tabular"`, a `Paginator` under `output="raw"`).
381+
382+ Pass a **list of GEOIDs** to query many blocks in one call (one request,
383+ one rate-limit tick): rows are the flat, `geoid`-tagged union of every
384+ origin's POIs, with per-origin `errors` / `truncated` / `truncated_reason`
385+ on `df.attrs`. The batch form is not paginated (`limit` is ignored)."""
386+ if isinstance (geoid , (list , tuple )):
387+ reply = self ._request ("POST" , "/v1/blocks/pois" , json = _clean (
388+ {"origins" : list (geoid ), "mode" : _as_list (mode ),
389+ "type" : _as_list (type ), "dest_id" : _as_list (dest_id ),
390+ "max_minutes" : max_minutes }))
391+ return self ._deliver (reply , geometry = True , output = output )
352392 return self ._deliver (Paginator (
353393 self , "GET" , f"/v1/blocks/{ geoid } /pois" ,
354394 _clean ({"mode" : mode , "type" : type , "dest_id" : dest_id ,
355395 "max_minutes" : max_minutes , "limit" : limit }),
356396 ), geometry = True , output = output )
357397
358398 def point_summary (
359- self , lat : float , lon : float , * , mode = None , type = None ,
399+ self , lat , lon = None , * , mode = None , type = None ,
360400 if_none_match : str | None = None , output : str | None = None ,
361401 ):
362402 """Like `block_summary`, but from the census block containing a lat/lon.
363403 The resolved block GEOID is echoed as `resolved_block` and broadcast to a
364- `geoid` column. A DataFrame, or a raw `Reply`."""
404+ `geoid` column. A DataFrame, or a raw `Reply`.
405+
406+ Pass a **list of points** as the first argument — each a `(lat, lon)` pair
407+ or a `{"lat": .., "lon": ..}` mapping — to query many points in one call.
408+ Each row is tagged with its `origin_lat` / `origin_lon`; resolved blocks,
409+ `errors`, and `truncated` ride on `df.attrs`."""
410+ if isinstance (lat , (list , tuple )):
411+ if lon is not None :
412+ raise ValueError (
413+ "For multiple points pass a list of (lat, lon) pairs as the "
414+ "first argument and leave lon unset."
415+ )
416+ reply = self ._request ("POST" , "/v1/point/summary" , json = _clean (
417+ {"origins" : _point_origins (lat ), "mode" : _as_list (mode ),
418+ "type" : _as_list (type )}))
419+ return self ._deliver (reply , geometry = False , key = "results" ,
420+ output = output )
365421 return self ._deliver (
366422 self ._get ("/v1/point/summary" ,
367423 {"lat" : lat , "lon" : lon , "mode" : mode , "type" : type },
@@ -370,12 +426,28 @@ def point_summary(
370426 )
371427
372428 def point_pois (
373- self , lat : float , lon : float , * , mode = None , type = None , dest_id = None ,
429+ self , lat , lon = None , * , mode = None , type = None , dest_id = None ,
374430 max_minutes = None , limit = None , output : str | None = None ,
375431 ):
376432 """Like `block_pois`, but from the block containing a lat/lon. A
377433 GeoDataFrame of points (a plain DataFrame under `output="tabular"`, a
378- `Paginator` under `output="raw"`)."""
434+ `Paginator` under `output="raw"`).
435+
436+ Pass a **list of points** as the first argument — each a `(lat, lon)` pair
437+ or a `{"lat": .., "lon": ..}` mapping — to query many points in one call.
438+ Each row is tagged with its `origin_lat` / `origin_lon`. The batch form is
439+ not paginated (`limit` is ignored)."""
440+ if isinstance (lat , (list , tuple )):
441+ if lon is not None :
442+ raise ValueError (
443+ "For multiple points pass a list of (lat, lon) pairs as the "
444+ "first argument and leave lon unset."
445+ )
446+ reply = self ._request ("POST" , "/v1/point/pois" , json = _clean (
447+ {"origins" : _point_origins (lat ), "mode" : _as_list (mode ),
448+ "type" : _as_list (type ), "dest_id" : _as_list (dest_id ),
449+ "max_minutes" : max_minutes }))
450+ return self ._deliver (reply , geometry = True , output = output )
379451 return self ._deliver (Paginator (
380452 self , "GET" , "/v1/point/pois" ,
381453 _clean ({"lat" : lat , "lon" : lon , "mode" : mode , "type" : type ,
@@ -408,12 +480,22 @@ def poi(self, dest_id: int, *, if_none_match: str | None = None,
408480 )
409481
410482 def poi_catchment (
411- self , dest_id : int , * , mode = None , block = None , max_minutes = None ,
483+ self , dest_id , * , mode = None , block = None , max_minutes = None ,
412484 limit = None , output : str | None = None ,
413485 ):
414486 """Every census block that can reach a POI, one row per (block, mode).
415487 A GeoDataFrame of block polygons (a plain DataFrame under
416- `output="tabular"`, a `Paginator` under `output="raw"`)."""
488+ `output="tabular"`, a `Paginator` under `output="raw"`).
489+
490+ Pass a **list of dest_ids** to query many POIs in one call (one request,
491+ one rate-limit tick): rows are the flat, `dest_id`-tagged union of every
492+ POI's catchment, with per-POI `errors` / `truncated` on `df.attrs`. The
493+ batch form is not paginated (`limit` is ignored)."""
494+ if isinstance (dest_id , (list , tuple )):
495+ reply = self ._request ("POST" , "/v1/pois/catchment" , json = _clean (
496+ {"dest_ids" : list (dest_id ), "mode" : _as_list (mode ),
497+ "block" : _as_list (block ), "max_minutes" : max_minutes }))
498+ return self ._deliver (reply , geometry = True , output = output )
417499 return self ._deliver (Paginator (
418500 self , "GET" , f"/v1/pois/{ dest_id } /catchment" ,
419501 _clean ({"mode" : mode , "block" : block , "max_minutes" : max_minutes ,
0 commit comments