def _is_endpoint(
    G: nx.MultiDiGraph,
    node: int,
    node_attrs_include: Iterable[str] | None,
    edge_attrs_differ: Iterable[str] | None,
) -> bool:
    """
    Determine if a node is a true endpoint of an edge.

    Return True if the node is a "true" endpoint of an edge in the network,
    otherwise False. OpenStreetMap data includes many nodes that exist only as
    geometric vertices to allow ways to curve. `node` is a true edge endpoint
    if it satisfies at least 1 of the following 5 rules:

    1) It is its own neighbor (ie, it self-loops).

    2) Or, it has no incoming edges or no outgoing edges (ie, all its incident
    edges are inbound or all its incident edges are outbound).

    3) Or, it does not have exactly two neighbors and degree of 2 or 4.

    4) Or, if `node_attrs_include` is not None and it has one or more of the
    attributes in `node_attrs_include`.

    5) Or, if `edge_attrs_differ` is not None and its incident edges have
    different values than each other for any of the edge attributes in
    `edge_attrs_differ`.

    Parameters
    ----------
    G
        Input graph.
    node
        The ID of the node to check.
    node_attrs_include
        Node attribute names for relaxing the strictness of endpoint
        determination. A node is always an endpoint if it possesses one or
        more of the attributes in `node_attrs_include`.
    edge_attrs_differ
        Edge attribute names for relaxing the strictness of endpoint
        determination. A node is always an endpoint if its incident edges have
        different values than each other for any attribute in
        `edge_attrs_differ`.

    Returns
    -------
    endpoint
        True if node is an endpoint, otherwise False.
    """
    neighbors = set(list(G.predecessors(node)) + list(G.successors(node)))
    n = len(neighbors)
    d = G.degree(node)

    # RULE 1
    # if the node appears in its list of neighbors, it self-loops: this is
    # always an endpoint
    if node in neighbors:
        return True

    # RULE 2
    # if node has no incoming edges or no outgoing edges, it is an endpoint
    if G.out_degree(node) == 0 or G.in_degree(node) == 0:
        return True

    # RULE 3
    # else, if it does NOT have 2 neighbors AND either 2 or 4 directed edges,
    # it is an endpoint. either it has 1 or 3+ neighbors, in which case it is
    # a dead-end or an intersection of multiple streets or it has 2 neighbors
    # but 3 degree (indicating a change from oneway to twoway) or more than 4
    # degree (indicating a parallel edge) and thus is an endpoint
    if not ((n == 2) and (d in {2, 4})):  # noqa: PLR2004
        return True

    # RULE 4
    # non-strict mode: does it contain an attr denoting that it is an endpoint
    if node_attrs_include is not None and len(set(node_attrs_include) & G.nodes[node].keys()) > 0:
        return True

    # RULE 5
    # non-strict mode: do its incident edges have different attr values? for
    # each attribute to check, collect the attribute's values in all inbound
    # and outbound edges. if there is more than 1 unique value then this node
    # is an endpoint
    if edge_attrs_differ is not None:
        for attr in edge_attrs_differ:
            in_values = {v for _, _, v in G.in_edges(node, data=attr, keys=False)}
            out_values = {v for _, _, v in G.out_edges(node, data=attr, keys=False)}
            if len(in_values | out_values) > 1:
                return True

    # if none of the preceding rules passed, then it is not an endpoint
    return False

    Determine if a node is a true endpoint of an edge.

    Return True if the node is a "true" endpoint of an edge in the network,
    otherwise False. OpenStreetMap data includes many nodes that exist only as
    geometric vertices to allow ways to curve. `node` is a true edge endpoint
    if it satisfies at least 1 of the following 5 rules:

    1) It is its own neighbor (ie, it self-loops).

    2) Or, it has no incoming edges or no outgoing edges (ie, all its incident
    edges are inbound or all its incident edges are outbound).

    3) Or, it does not have exactly two neighbors and degree of 2 or 4.

    4) Or, if `node_attrs_include` is not None and it has one or more of the
    attributes in `node_attrs_include`.

    5) Or, if `edge_attrs_differ` is not None and its incident edges have
    different values than each other for any of the edge attributes in
    `edge_attrs_differ`.

    Parameters
    ----------
    G
        Input graph.
    node
        The ID of the node to check.
    node_attrs_include
        Node attribute names for relaxing the strictness of endpoint
        determination. A node is always an endpoint if it possesses one or
        more of the attributes in `node_attrs_include`.
    edge_attrs_differ
        Edge attribute names for relaxing the strictness of endpoint
        determination. A node is always an endpoint if its incident edges have
        different values than each other for any attribute in
        `edge_attrs_differ`.

    Returns
    -------
    endpoint
        True if node is an endpoint, otherwise False.
    
osmnx/simplification.py