# How `get_perimeter`, `get_boundary`, and `parse_query` Work Together in prettymaps Fetch.py

> Discover how prettymaps fetch.py functions get_perimeter, get_boundary, and parse_query collaborate to convert your query into a projected polygon for precise map bounds.

- Repository: [Marcelo de Oliveira Rosa Prates/prettymaps](https://github.com/marceloprates/prettymaps)
- Tags: internals
- Published: 2026-08-20

---

**The three helper functions in [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py)—`parse_query`, `get_boundary`, and `get_perimeter`—transform any user query into a properly projected polygon that defines the map's geographic bounds.**

`parse_query` identifies the input type. `get_boundary` creates radial shapes when a radius is specified. `get_perimeter` orchestrates the final perimeter, handling OSM lookups, aspect ratio scaling, and optional dilation. Together they form the foundation of Prettymaps' geographic query handling.

## Overview of the Perimeter Pipeline

Every Prettymaps visualization starts with a perimeter—a polygon that defines what area to fetch from OpenStreetMap. In [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py), the three functions collaborate to normalize diverse inputs into a consistent output.

The pipeline handles four query types:

- **GeoDataFrame** – a pre-built polygon provided directly
- **Coordinates** – a `(longitude, latitude)` tuple
- **OSM ID** – a string like `"R146236"` for relations, `"W123"` for ways
- **Address** – any free-form place name

This design lets users specify locations flexibly while ensuring downstream code always receives a valid,EPSG:4326-projected polygon.

## Step 1: `parse_query` Identifies the Input Type

`parse_query` (lines 87–97 in [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py)) inspects the query and returns a discriminator string used by later functions.

```python
from prettymaps.fetch import parse_query

parse_query("Central Park, NYC")      # returns "address"

parse_query((-73.9654, 40.7829))      # returns "coordinates"

parse_query("R1204843")               # returns "osmid"

parse_query(my_geodataframe)          # returns "polygon"

```

The function uses simple type checking:

- `isinstance(query, tuple)` → `"coordinates"`
- `isinstance(query, str)` starting with `R`, `W`, or `N` → `"osmid"`
- `isinstance(query, str)` → `"address"`
- Has `.geometry` attribute → `"polygon"`

This classification determines which path `get_perimeter` takes next.

## Step 2: `get_boundary` Creates Radial Geometries

When a `radius` is provided with coordinates or an address, `get_boundary` (lines 99–128) generates the actual geometry. It produces either circular buffers or rotated square bounds.

The function follows this sequence:

1. **Geocode if needed** – For addresses or OSM IDs, use `ox.geocoder.geocode` to resolve to a point
2. **Project to UTM** – Use `ox.projection.project_gdf` to work in meters
3. **Build shape** – Circle via `buffer(radius)` or square via custom polygon construction
4. **Apply rotation** – Optional rotation for square bounds
5. **Unproject to WGS84** – Return to EPSG:4326

```python
from prettymaps.fetch import get_boundary

# Circle with 500m radius around coordinates

circle = get_boundary(
    query=(-73.9855, 40.7580),
    radius=500,
    circle=True,
    dilate=0
)

# Square rotated 45 degrees, 300m from center

square = get_boundary(
    query="Times Square, NYC",
    radius=300,
    circle=False,
    rotation=45,
    dilate=0
)

```

The UTM projection is critical: buffering in degrees produces distorted shapes. By temporarily projecting to the appropriate UTM zone, the buffer distance in meters translates to accurate real-world dimensions.

## Step 3: `get_perimeter` Orchestrates Final Output

`get_perimeter` (lines 132–173 in [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py)) is the public interface. It decides whether to call `get_boundary` or handle the query directly, then applies final transformations.

**Decision branch:**

| Condition | Action |
|-----------|--------|
| `radius` provided | Delegate to `get_boundary` |
| Query is GeoDataFrame | Return as-is |
| Otherwise | Fetch polygon via `ox.geocoder.geocode_to_gdf` |

**Post-processing steps:**

1. Project to UTM for geometric operations
2. If `aspect_ratio` specified, scale to maintain proportions
3. Re-project to EPSG:4326
4. If `dilate` > 0, apply final buffer expansion

```python
from prettymaps.fetch import get_perimeter

# Fetch exact OSM boundary (no radius)

city = get_perimeter("Manhattan, New York", radius=None)

# 1km circle with 10% dilation

buffered = get_perimeter(
    query="R2612945",  # Manhattan relation ID

    radius=1000,
    circle=True,
    dilate=100  # additional 100m buffer

)

# Custom aspect ratio for print dimensions

wide_map = get_perimeter(
    query=(2.3522, 48.8566),  # Paris

    radius=800,
    aspect_ratio=2.0  # 2:1 width:height

)

```

The `aspect_ratio` parameter is particularly useful for controlling the final map's proportions regardless of the geographic region's shape.

## How the Functions Connect in Practice

The typical call chain in `prettymaps.get_gdfs` looks like:

```python

# Simplified excerpt from prettymaps/fetch.py workflow

query = "R146236"  # Central Park relation

radius = 500

# 1. Classify

query_type = parse_query(query)  # → "osmid"

# 2. Build or fetch perimeter

perimeter = get_perimeter(
    query=query,
    radius=radius,
    circle=True,
    aspect_ratio=None,
    dilate=0
)

# Internally calls get_boundary for radial generation

# 3. Use perimeter for all layer queries

# All OSM requests are clipped to this geometry

```

The perimeter becomes the spatial filter for every layer in the `layers_dict` passed to `get_gdfs`. This ensures building footprints, roads, water, and land use all align to the same geographic extent.

## Complete Working Example

```python
import prettymaps
from prettymaps.fetch import parse_query, get_boundary, get_perimeter

# --- Step by step exploration ---

# 1. Parse different query types

print(parse_query("Golden Gate Bridge"))           # address

print(parse_query((-122.4783, 37.8199)))           # coordinates  

print(parse_query("W27164876"))                    # osmid (way)

# 2. Compare boundary shapes around same point

coords = (-122.4194, 37.7749)  # SF City Hall

circle_boundary = get_boundary(coords, radius=400, circle=True)
square_boundary = get_boundary(coords, radius=400, circle=False, rotation=30)

print(f"Circle area: {circle_boundary.area:.6f} degrees²")
print(f"Square area: {square_boundary.area:.6f} degrees²")

# 3. Get final perimeters with different configurations

exact_sf = get_perimeter("San Francisco, CA", radius=None)  # OSM boundary

radial_sf = get_perimeter(
    "San Francisco, CA",
    radius=2000,
    circle=False,
    aspect_ratio=1.5,
    dilate=100
)

# 4. Use in full Prettymaps workflow

layers = {
    "perimeter": {},
    "streets": {"tags": {"highway": True}},
    "buildings": {"tags": {"building": True}}
}

# get_gdfs calls get_perimeter internally for the perimeter layer

gdfs = prettymaps.get_gdfs("Mission District, SF", layers, radius=800)

```

## Summary

- **`parse_query`** (lines 87–97) classifies inputs as `"polygon"`, `"coordinates"`, `"osmid"`, or `"address"` through simple type inspection.

- **`get_boundary`** (lines 99–128) creates circular or square/rotated geometries around points by projecting to UTM, buffering in meters, and returning to EPSG:4326.

- **`get_perimeter`** (lines 132–173) orchestrates the final output, choosing between boundary generation, direct polygon passthrough, or OSM polygon lookup, with optional aspect ratio scaling and dilation.

- Together in [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py), these functions ensure any location query resolves to a consistent, properly projected perimeter that drives all subsequent OpenStreetMap data retrieval.

## Frequently Asked Questions

### What happens if I provide both a GeoDataFrame and a radius?

**`get_perimeter` ignores the radius when the query is already a GeoDataFrame.** The `"polygon"` type check takes precedence (line ~138), returning the supplied geometry unchanged. If you need to buffer an existing polygon, apply `geopandas.GeoSeries.buffer()` before passing it.

### Why does `get_boundary` project to UTM instead of using EPSG:4326 directly?

**Geometric operations like buffering require linear units (meters), but EPSG:4326 uses degrees.** A degree of longitude varies from ~111km at the equator to 0 at the poles. By projecting to the appropriate UTM zone via `ox.projection.project_gdf`, the code ensures a 500-meter radius is actually 500 meters regardless of latitude.

### How do `aspect_ratio` and `dilate` interact in `get_perimeter`?

**Scaling to `aspect_ratio` happens before dilation.** In the implementation (lines ~155–162), the geometry is first projected to UTM, then scaled to meet the requested width-to-height ratio, then re-projected. The `dilate` buffer (lines ~170–172) applies as a final uniform expansion in the UTM projection. Order matters: dilating first then scaling would create non-uniform buffer widths.