Mapping Ice Cream Shops Using R and OpenStreetMap

R
mapping
leaflet
geospatial
Author
Published

December 9, 2025

Introduction

In this post I’ll show how you can use R and OpenStreetMap data to map the locations of all the ice-cream shops in a city! I’ll use the {osmdata} package ( et al. (2017)) to get the locations of ice-cream shops in Denver, and then plot them on a map.

Getting ice-cream locations from OpenStreetMap

Defining the Bounding box

First we need to define the area to search for our locations of interest. The {osmdata} package provides a function getbb() to return the bounding box for a city or location. The output can be returned in sf format to easily plot (figure Figure 1).

Code
# Make bounding box for Denver

denver_bb <-
  getbb(place_name = "Denver,CO" ,
        format_out = "sf_polygon") 


leaflet() |>
  addTiles() |>
  addPolygons(data = denver_bb,
              color = "black")
Figure 1: Bounding box for Denver, CO

Finding the ice cream

How do we find just ice-cream shops in the OpenStreetMap data? The list of available features and tags are available from the functions available_features() and available_tags(). In this case, the feature of interest is “amenity”, which has 137 tag options, including “ice_cream”.

Putting it all together

To get the ice-cream locations, we construct an overpass query object (opq):

  • First we specify the bounding box using the getbb() function.

  • Next we create an overpass query object with opq()

  • Then we specify which features we want with the add_osm_feature() function.

  • Finally we call the osmdata_sf() function to retrieve the data.

This returns an object of class osmdata which contains information about the API call as well as the results. The locations we are interested in will be in $osm_points .

Code
denver_ice_cream <- 
  getbb(place_name = "Denver, CO") |>
  opq() |>
  add_osm_feature(key = "amenity", value = "ice_cream") |>
  osmdata_sf()

denver_ice_cream
Object of class 'osmdata' with:
                 $bbox : 39.6143008,-105.1098845,39.9142087,-104.5996997
        $overpass_call : The call submitted to the overpass API
                 $meta : metadata including timestamp and version numbers
           $osm_points : 'sf' Simple Features Collection with 170 points
            $osm_lines : NULL
         $osm_polygons : 'sf' Simple Features Collection with 7 polygons
       $osm_multilines : NULL
    $osm_multipolygons : NULL

$osm_points is a simple feature collection; basically a data frame with geometry added. We can see there are a lot of features/variables, which are missing for most locations.

Code
denver_ice_cream$osm_points |> filter(!is.na(name)) |> glimpse()
Rows: 92
Columns: 60
$ osm_id                     <chr> "631845194", "967331912", "1030196174", "15…
$ name                       <chr> "Ben & Jerry's", "Cold Stone Creamery", "Sw…
$ `addr:city`                <chr> "Denver", NA, "Denver", "Lakewood", NA, "De…
$ `addr:housename`           <chr> NA, NA, NA, NA, NA, NA, NA, "799", NA, NA, …
$ `addr:housenumber`         <chr> "2339", NA, "52", "9910", NA, NA, NA, "799"…
$ `addr:postcode`            <chr> "80210", NA, "80203", "80123", NA, "80238",…
$ `addr:state`               <chr> "CO", NA, "CO", "CO", NA, "CO", "CO", "CO",…
$ `addr:street`              <chr> "East Evans Avenue", NA, "Broadway", "West …
$ `addr:unit`                <chr> NA, NA, NA, NA, NA, NA, NA, NA, "101", NA, …
$ air_conditioning           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ amenity                    <chr> "ice_cream", "ice_cream", "ice_cream", "ice…
$ brand                      <chr> "Ben & Jerry's", "Cold Stone Creamery", NA,…
$ `brand:website`            <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `brand:wikidata`           <chr> "Q816412", "Q1094923", NA, "Q8054428", "Q10…
$ building                   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ check_date                 <chr> NA, NA, NA, NA, NA, NA, NA, "2024-12-30", N…
$ `check_date:opening_hours` <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ craft                      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ cuisine                    <chr> "ice_cream", "ice_cream", NA, "frozen_yogur…
$ delivery                   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ description                <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ designation                <chr> NA, NA, NA, "frozen yogurt", NA, NA, NA, NA…
$ `diet:halal`               <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `diet:kosher`              <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `diet:vegan`               <chr> NA, NA, "yes", NA, NA, NA, NA, NA, NA, NA, …
$ `diet:vegetarian`          <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ door                       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ drive_through              <chr> NA, NA, NA, NA, NA, NA, NA, "no", NA, NA, N…
$ emergency                  <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ entrance                   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ facebook                   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `fire_hydrant:type`        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ indoor                     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ indoor_seating             <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ level                      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ mobile                     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `name:en`                  <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ note                       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ old_name                   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ opening_hours              <chr> NA, NA, "Mo-Su 13:00-24:00", NA, NA, NA, NA…
$ outdoor_seating            <chr> NA, NA, NA, "yes", NA, NA, NA, "yes", NA, N…
$ `outdoor_seating:comfort`  <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `payment:cash`             <chr> NA, NA, NA, NA, NA, NA, NA, "yes", NA, NA, …
$ `payment:credit_cards`     <chr> NA, NA, NA, NA, NA, NA, NA, "yes", NA, NA, …
$ `payment:debit_cards`      <chr> NA, NA, NA, NA, NA, NA, NA, "yes", NA, NA, …
$ phone                      <chr> "+1-303-733-8878", NA, "+1-720-994-5203", "…
$ ref                        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ shop                       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ short_name                 <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ smoking                    <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ source                     <chr> NA, NA, NA, "ground_survey", "local_knowled…
$ start_date                 <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ takeaway                   <chr> NA, NA, NA, "yes", NA, NA, NA, NA, NA, NA, …
$ toilets                    <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ `toilets:access`           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ weather_protection         <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ website                    <chr> "https://www.benjerry.com/uofdenver", NA, "…
$ `website:menu`             <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ wheelchair                 <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ geometry                   <POINT [°]> POINT (-104.9597 39.67862), POINT (-1…

I’ll filter out the locations without names, and then plot the remaining locations on a leaflet map ( Figure 2).

You might notice that some of the locations returned are outside of the sf polygons returned by getbb() we looked at earlier (Figure 1). The {osmdata} query actually uses the rectangular bounding box that contains these polygons. I’ve plotted that box here also; the Denver airport (to the northeast) extends that box pretty far north.

Code
# filter out locations with NA names
denver_ice_cream_points <- denver_ice_cream$osm_points |>
  filter(!is.na(name))

# get rectangular bounding box
bb = st_as_sfc(st_bbox(denver_bb))

leaflet() |>
  addTiles() |>
  addMarkers(data = denver_ice_cream_points,
             popup = ~name) |>
  addPolygons(data = denver_bb,
              color = "black") |>
  addPolygons(data = bb,
              color = "gray",
              fillColor = "transparent")
Figure 2: Map of ice-cream shop locations in Denver, CO from Openstreetmap.

What are the most common ice-cream shops? Looks like Coors Field has a lot of ice cream stands, and they are all treated as separate shops in the data. The second most-frequent shop is Cold Stone Creamery.

Code
denver_ice_cream_points |>
  sf::st_drop_geometry() |>
  count(name, sort = TRUE) |>
  filter(n > 1) |>
  DT::datatable()
Table 1: Most frequent ice-cream shop names in Denver.

Summary

In this post I used the {osmdata} package to get the locations of ice-cream shops in Denver from OpenStreetMap, and plot them on a map. The same techniques could be used to locate, map, and analyze all kinds of different features from OpenStreetMap. I hope you found this informational and interesting!

SessionInfo

R version 4.4.1 (2024-06-14)
Platform: x86_64-apple-darwin20
Running under: macOS 15.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Denver
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
[1] leaflet_2.2.3 osmdata_0.2.5 sf_1.0-21     dplyr_1.1.4  

loaded via a namespace (and not attached):
 [1] jsonlite_2.0.0     compiler_4.4.1     renv_1.0.9         tidyselect_1.2.1  
 [5] Rcpp_1.1.0         xml2_1.3.8         jquerylib_0.1.4    yaml_2.3.10       
 [9] fastmap_1.2.0      R6_2.6.1           generics_0.1.4     curl_6.4.0        
[13] classInt_0.4-11    httr2_1.2.1        knitr_1.50         htmlwidgets_1.6.4 
[17] tibble_3.3.0       units_0.8-7        lubridate_1.9.4    DBI_1.2.3         
[21] bslib_0.9.0        pillar_1.11.0      rlang_1.1.6        DT_0.34.0         
[25] cachem_1.1.0       xfun_0.52          sass_0.4.10        timechange_0.3.0  
[29] cli_3.6.5          magrittr_2.0.4     class_7.3-23       crosstalk_1.2.2   
[33] digest_0.6.37      grid_4.4.1         rstudioapi_0.17.1  rappdirs_0.3.3    
[37] lifecycle_1.0.4    vctrs_0.6.5        KernSmooth_2.23-26 proxy_0.4-27      
[41] evaluate_1.0.5     glue_1.8.0         e1071_1.7-16       rmarkdown_2.29    
[45] tools_4.4.1        pkgconfig_2.0.3    htmltools_0.5.8.1 

References

Mark Padgham, Bob Rudis, Robin Lovelace, and Maëlle Salmon. 2017. “Osmdata” 2: 305. https://doi.org/10.21105/joss.00305.