def _process_features(
    elements: list[dict[str, Any]],
    query_tag_keys: set[str],
) -> list[dict[str, Any]]:
    """
    Convert node/way/relation elements into features with geometries.

    Parameters
    ----------
    elements
        The node/way/relation elements retrieved from the server.
    query_tag_keys
        The keys of the tags used to query for matching features.

    Returns
    -------
    features
        The features with geometries.
    """
    nodes = []  # all nodes, including ones that just compose ways
    feature_nodes = []  # nodes that possibly match our query tags
    node_coords = {}  # hold node lon,lat tuples to create way geoms
    ways = []  # all ways, including ones that just compose relations
    feature_ways = []  # ways that possibly match our query tags
    way_geoms = {}  # hold way geoms to create relation geoms
    relations = []  # all relations

    # sort elements by node, way, and relation. only retain relations that
    # match the relation types we currently handle. remove any geometry tags
    # (they shouldn't exist) or they'll overwrite our geom attributes later
    for element in elements:
        element.get("tags", {}).pop("geometry", None)
        et = element["type"]
        if et == "node":
            nodes.append(element)
        elif et == "way":
            ways.append(element)
        elif et == "relation" and element.get("tags", {}).get("type") in _RELATION_TYPES:
            relations.append(element)

    # extract all nodes' coords, then add to features any nodes with tags that
    # match the passed query tags, or with any tags if no query tags passed
    for node in nodes:
        node_coords[node["id"]] = (node["lon"], node["lat"])
        if (len(query_tag_keys) == 0 and len(node.get("tags", {}).keys()) > 0) or (
            len(query_tag_keys & node.get("tags", {}).keys()) > 0
        ):
            node["element"] = node.pop("type")
            node["geometry"] = Point(node.pop("lon"), node.pop("lat"))
            node.update(node.pop("tags"))
            feature_nodes.append(node)

    # build all ways' geometries, then add to features any ways with tags that
    # match the passed query tags, or with any tags if no query tags passed
    for way in ways:
        way["geometry"] = _build_way_geometry(
            way["id"],
            way.pop("nodes"),
            way.get("tags", {}),
            node_coords,
        )
        way_geoms[way["id"]] = way["geometry"]
        if (len(query_tag_keys) == 0 and len(way.get("tags", {}).keys()) > 0) or (
            len(query_tag_keys & way.get("tags", {}).keys()) > 0
        ):
            way["element"] = way.pop("type")
            way.update(way.pop("tags"))
            feature_ways.append(way)

    # process relations and build their geometries
    for relation in relations:
        relation["element"] = "relation"
        relation.update(relation.pop("tags"))
        relation["geometry"] = _build_relation_geometry(relation.pop("members"), way_geoms)

    features = [*feature_nodes, *feature_ways, *relations]
    if len(features) == 0:
        msg = "No matching features. Check query location, tags, and log."
        raise InsufficientResponseError(msg)

    return features

    Convert node/way/relation elements into features with geometries.

    Parameters
    ----------
    elements
        The node/way/relation elements retrieved from the server.
    query_tag_keys
        The keys of the tags used to query for matching features.

    Returns
    -------
    features
        The features with geometries.
    
osmnx/features.py