Jan 2026
Alternate Side Parking
Free Parking in NYC can be found if you’re willing to navigate the world of “Alternate Side Parking” (ASP). A couple times per week, parkers need to accommodate street-sweeping trucks by moving their cars to the “alternate side” of the street. Ask any NYC street-parker about this and they’ll bore you with details of their little weekly routine.
Most of this free parking is in residential neighborhoods, but surprisingly, you can find it just about anywhere. If you know where to look, you’ll find spots in Midtown, SoHo, Chinatown, FiDi, Union Square, Flushing etc.
And when I say you’ll find these spots anywhere - technically true, but the spots you find will be occupied, as free parking is at nearly 100% utilization around the clock, especially in busy and/or commercial areas.
However, if you’re riding a Vespa or motorcycle, you don’t have this problem — you can always find space backed-in between parked cars (this scenario is the actual motivation for this post/app).
On two wheels, you can park anywhere in NYC for free at any time. Given this, I continue to be surprised at the low numbers of Vespas/motorcycles in NYC. Though, car drivers are insane and most people value their lives so perhaps that’s a reason.
Anyways, where are these spots? There isn’t a free parking map provided by the city. The NYC DOT provides this map, which has some fidgets and widgets that don’t seem to solve our problem.
So let’s make a map: parkgratis.com
Building the Map
All the code for this map is available here, and is separated into two areas - app and data_processing.
app builds the website and data_processing does … data processing.
I’ll spare you the details — feed the repo into your favorite LLM if you want to dig in — rather I’ll touch on a few points I think are worth mentioning
Data
NYC Open Data provides this dataset.
This file contains all Parking Regulation Locations and Signs in NYC. There are 440,363 rows, corresponding to 440,363 parking signs.
Here is an abridged sample:
| borough | on_street | sign_description | sign_x_coord | sign_y_coord |
|---|---|---|---|---|
| Manhattan | 3 AVENUE | 2 HMP SATURDAY 8AM-7PM <-> | 996877 | 222815 |
| Bronx | EAST 217 STREET | TRUCK (SYMBOL) TRUCK LOADING ONLY MONDAY-FRIDAY 8AM-6PM –> (SUPERSEDES SP-449CA & SP-70BA) | 1022145 | 260647 |
| Manhattan | 3 AVENUE | 1 HMP COMMERCIAL VEHICLES ONLY MONDAY-FRIDAY 8AM-7PM <-> | 996877 | 222815 |
| Queens | NORTHERN BOULEVARD | LOCAL MTA BUS ROUTE PANEL (TEXT TO BE MODIFIED AS REQUESTED) | 1057386 | 219889 |
| Manhattan | HUDSON STREET | NO PARKING (SANITATION BROOM SYMBOL) 7:30AM-8AM EXCEPT SUNDAY –> (SUPERSEDES SP-139CA & PS-23BA) | 982560 | 207047 |
This snippet only shows a handful of columns, but the full set has 25 columns:
"order_number","record_type","borough","on_street","from_street","to_street","side_of_street","sign_code","sign_description","distance_from_intersection","sign_x_coord","sign_y_coord"
Alternate Side Parking (ASP)
Unfortunately, none of the columns mention anything about “Free Parking”, because the truth is a bit more complicated.
Free parking sometimes exists on streets that have ASP
Some streets (or sections of streets) can have multiple overlapping signs; so a street with ASP doesn’t guarantee free parking.
Looking at the sample data in the sign_description column, we can equate ASP with the text “Broom Symbol” (ie street sweeping)
When is ASP free?
My heuristic, which seems right 95% of the time, is that ASP zones with:
- 90 minute ranges are free
- Shorter ranges (30 minutes) tend to come with additional restrictions (ie parking meters, special permits, etc).
- Longer ranges (4 hours) are hard to predict. Sometimes free, sometimes not.
- Overnight restrictions are also hard to predict. Sometimes free, sometimes not.
I’ve found this approach to be pretty reliable in practice (but suggestions welcome).
Data Processing
We have 440,363 rows that we need to parse, looking for “BROOM SYMBOL” with a 90 minute range.
Regex handles most of the heavy lifting here. You can see the details here.
For our output file, we don’t need all 25 columns of data - we just mapping these locations eventually, so we just bring along:
'borough',
'on_street',
'from_street',
'to_street',
'sign_description',
'sign_x_coord_lat',
'sign_y_coord_lon'
Dupes
On any given city block, there can be multiple signs saying the same thing at different locations on that block. For performance and UI reasons, we only want to display one of these signs per block.
To do this, we must check for duplicates:
# outside loop
seen_keys = Set.new
# inside loop (of 441k rows)
key = [
borough,
row['on_street'].to_s.strip,
row['from_street'].to_s.strip,
row['to_street'].to_s.strip,
desc.to_s.strip
]
next if seen_keys.include?(key)
seen_keys << key
On any given block, all signs will have the same on_street, from_street, to_street, and borough. And we need to check the sign description desc to make sure it’s actually a duplicate.
To do this in Ruby, a Set datatype is ideal for identifying duplicates, we just fill it like an array, and checking for presence of data is very fast.
Coordinates
Eventually we’ll plot these signs on a map, and if we look back to the sample data, we’ll see that we have sign_x_coord and sign_y_coord.
Only thing is they don’t appear to be latitude and longitude. Rather they are EPSG:2263 coordinates (EPSG is the system, and 2263 corresponds to the NYC region).
EPSG is an alternate system meant for hyper-local location targeting - which in this case - signs on a city block - seems to be a valid use case.
We need to convert these coordinates to lat/long to feed into Google maps - here’s our converter.
App
The processed data is exported to a CSV file that we load into our frontend. It’s all fairly straightforward HTML, Javascript, Google Maps Apis, with most of the logic living here.
Frontend Performance
We started with 440,363 rows in our original dataset, and after filtering for 90 minute ASP and removing dupes, we have 57,587 rows in our CSV.
If we try to add 57,587 markers onto a Google Map, our browser will explode.
Instead, we have a couple of performance-related tricks.
- Segment all the markers by borough, and default to showing Manhattan markers. You can still enable all 5 boroughs using the checkboxes, but at least on initial load, we reduce loading by 89% (only 11% of signs are in Manhattan).
- Use heat maps when zoomed out. We determined a zoom threshold where showing individual location markers would render quickly (
const MARKER_ZOOM_THRESHOLD = 16;), and when zoomed out farther than that, we’ll display a heat map of the markers, rather than the markers themselves. Heat maps are performant and useful for large chunks of location data. It’s still important to have the individual markers at an appropriate zoom level, so that we can see information regarding that sign, and even pop into a street view if need be.
The end
Ride safe and free park.