def geocode_to_gdf(
    query: str | dict[str, str] | list[str | dict[str, str]],
    *,
    which_result: int | None | list[int | None] = None,
    by_osmid: bool = False,
) -> gpd.GeoDataFrame:
    """
    Retrieve OSM elements by place name or OSM ID with the Nominatim API.

    If searching by place name, the `query` argument can be a string or
    structured dict, or a list of such strings/dicts to send to the geocoder.
    This uses the Nominatim "search" endpoint to geocode the place name to the
    best-matching OSM element, then returns that element and its attribute
    data.

    You can instead query by OSM ID by passing `by_osmid=True`. This uses the
    Nominatim "lookup" endpoint to retrieve the OSM element with that ID. In
    this case, the function treats the `query` argument as an OSM ID (or list
    of OSM IDs), which must be prepended with their types: node (N), way (W),
    or relation (R) in accordance with the Nominatim API format. For example,
    `query=["R2192363", "N240109189", "W427818536"]`.

    If `query` is a list, then `which_result` must be either an int or a list
    with the same length as `query`. The queries you provide must be
    resolvable to elements in the Nominatim database. The resulting
    GeoDataFrame's geometry column contains place boundaries if they exist.

    Parameters
    ----------
    query
        The query string(s) or structured dict(s) to geocode.
    which_result
        Which search result to return. If None, auto-select the first
        (Multi)Polygon or raise an error if OSM doesn't return one. To get
        the top match (sorted by importance) regardless of geometry type, set
        `which_result=1`. Ignored if `by_osmid=True`.
    by_osmid
        If True, treat query as an OSM ID lookup rather than text search.

    Returns
    -------
    gdf
        GeoDataFrame with one row for each query result.
    """
    if isinstance(query, list):
        # if query is a list of queries but which_result is int/None, then
        # turn which_result into a list with same length as query list
        q_list = query
        wr_list = which_result if isinstance(which_result, list) else [which_result] * len(query)
    else:
        # if query is not already a list, turn it into one
        # if which_result was a list, take 0th element, otherwise make it list
        q_list = [query]
        wr_list = [which_result[0]] if isinstance(which_result, list) else [which_result]

    # ensure same length
    if len(q_list) != len(wr_list):  # pragma: no cover
        msg = "`which_result` length must equal `query` length."
        raise ValueError(msg)

    # geocode each query, concat as GeoDataFrame rows, then set the CRS
    results = (_geocode_query_to_gdf(q, wr, by_osmid) for q, wr in zip(q_list, wr_list))
    gdf = pd.concat(results, ignore_index=True).set_crs(settings.default_crs)

    msg = f"Created GeoDataFrame with {len(gdf)} rows from {len(q_list)} queries"
    utils.log(msg, level=lg.INFO)
    return gdf

    Retrieve OSM elements by place name or OSM ID with the Nominatim API.

    If searching by place name, the `query` argument can be a string or
    structured dict, or a list of such strings/dicts to send to the geocoder.
    This uses the Nominatim "search" endpoint to geocode the place name to the
    best-matching OSM element, then returns that element and its attribute
    data.

    You can instead query by OSM ID by passing `by_osmid=True`. This uses the
    Nominatim "lookup" endpoint to retrieve the OSM element with that ID. In
    this case, the function treats the `query` argument as an OSM ID (or list
    of OSM IDs), which must be prepended with their types: node (N), way (W),
    or relation (R) in accordance with the Nominatim API format. For example,
    `query=["R2192363", "N240109189", "W427818536"]`.

    If `query` is a list, then `which_result` must be either an int or a list
    with the same length as `query`. The queries you provide must be
    resolvable to elements in the Nominatim database. The resulting
    GeoDataFrame's geometry column contains place boundaries if they exist.

    Parameters
    ----------
    query
        The query string(s) or structured dict(s) to geocode.
    which_result
        Which search result to return. If None, auto-select the first
        (Multi)Polygon or raise an error if OSM doesn't return one. To get
        the top match (sorted by importance) regardless of geometry type, set
        `which_result=1`. Ignored if `by_osmid=True`.
    by_osmid
        If True, treat query as an OSM ID lookup rather than text search.

    Returns
    -------
    gdf
        GeoDataFrame with one row for each query result.
    
osmnx/geocoder.py