docs: def create_footprints_gdf(polygon=None, north=None, south=None, east=None, west=None, 
                            footprint_type='building', retain_invalid=False):
      """
      Get footprint data from OSM then assemble it into a GeoDataFrame.
  
      Parameters
      ----------
      polygon : shapely Polygon or MultiPolygon
          geographic shape to fetch the footprints within
      north : float
          northern latitude of bounding box
      south : float
          southern latitude of bounding box
      east : float
          eastern longitude of bounding box
      west : float
          western longitude of bounding box
      footprint_type : string
          type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
      retain_invalid : bool
          if False discard any footprints with an invalid geometry
  
      Returns
      -------
      GeoDataFrame
      """
  
      responses = osm_footprints_download(polygon, north, south, east, west, footprint_type)
  
      # list of polygons to removed at the end of the process
      pop_list = []
  
      vertices = {}
      for response in responses:
          for result in response['elements']:
              if 'type' in result and result['type']=='node':
                  vertices[result['id']] = {'lat' : result['lat'],
                                            'lon' : result['lon']}
  
      footprints = {}
      for response in responses:
          for result in response['elements']:
              if 'type' in result and result['type']=='way':
                  nodes = result['nodes']
                  try:
                      polygon = Polygon([(vertices[node]['lon'], vertices[node]['lat']) for node in nodes])
                  except Exception:
                      log('Polygon has invalid geometry: {}'.format(nodes))
                  footprint = {'nodes' : nodes,
                              'geometry' : polygon}
  
                  if 'tags' in result:
                      for tag in result['tags']:
                          footprint[tag] = result['tags'][tag]
  
                  # if polygons are untagged or not tagged with the footprint_type
                  # add them to pop_list to be removed from the final dictionary
                  if 'tags' not in result:
                      pop_list.append(result['id'])
                  elif footprint_type not in result['tags']:
                      pop_list.append(result['id'])
  
                  footprints[result['id']] = footprint
  
      # Create multipolygon footprints and pop untagged supporting polygons from footprints
      for response in responses:
          for result in response['elements']:
              if 'type' in result and result['type']=='relation':
  
                  outer_polys = []
                  inner_polys = []
                  multipoly = []
  
                  for member in result['members']:
                      if 'role' in member and member['role']=='outer':
                          outer_polys.append(member['ref'])
                      if 'role' in member and member['role']=='inner':
                          inner_polys.append(member['ref'])
  
                  # osm allows multiple outer polygons in a relation
                  for outer_poly in outer_polys:
                      temp_poly=footprints[outer_poly]['geometry']
  
                      for inner_poly in inner_polys:
                          temp_poly=temp_poly.difference(footprints[inner_poly]['geometry'])
  
                      multipoly.append(temp_poly)
  
                  footprint = {'geometry' : MultiPolygon(multipoly)}
  
                  if 'tags' in result:
                      for tag in result['tags']:
                          footprint[tag] = result['tags'][tag]
  
                  footprints[result['id']] = footprint
  
      # remove supporting geometry from footprints dictionary
      for item in pop_list:
          footprints.pop(item)
  
      gdf = gpd.GeoDataFrame(footprints).T
      gdf.crs = settings.default_crs
  
      if not retain_invalid:
          # drop all invalid geometries
          gdf = gdf[gdf['geometry'].is_valid]
  
      return gdf