docs: def is_endpoint(G, node, strict=True):
      """
      Return True if the node is a "real" endpoint of an edge in the network, \
      otherwise False. OSM data includes lots of nodes that exist only as points \
      to help streets bend around curves. An end point is a node that either: \
      1) is its own neighbor, ie, it self-loops. \
      2) or, has no incoming edges or no outgoing edges, ie, all its incident \
          edges point inward or all its incident edges point outward. \
      3) or, it does not have exactly two neighbors and degree of 2 or 4. \
      4) or, if strict mode is false, if its edges have different OSM IDs. \
  
      Parameters
      ----------
      G : networkx multidigraph
  
      node : int
          the node to examine
      strict : bool
          if False, allow nodes to be end points even if they fail all other rules \
          but have edges with different OSM IDs
  
      Returns
      -------
      bool
  
      """
      neighbors = set(list(G.predecessors(node)) + list(G.successors(node)))
      n = len(neighbors)
      d = G.degree(node)
  
      if node in neighbors:
          # if the node appears in its list of neighbors, it self-loops. this is
          # always an endpoint.
          return True
  
      # if node has no incoming edges or no outgoing edges, it must be an endpoint
      elif G.out_degree(node)==0 or G.in_degree(node)==0:
          return True
  
      elif not (n==2 and (d==2 or d==4)):
          # 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
          return True
  
      elif not strict:
          # non-strict mode
          osmids = []
  
          # add all the edge OSM IDs for incoming edges
          for u in G.predecessors(node):
              for key in G[u][node]:
                  osmids.append(G.edges[u, node, key]['osmid'])
  
          # add all the edge OSM IDs for outgoing edges
          for v in G.successors(node):
              for key in G[node][v]:
                  osmids.append(G.edges[node, v, key]['osmid'])
  
          # if there is more than 1 OSM ID in the list of edge OSM IDs then it is
          # an endpoint, if not, it isn't
          return len(set(osmids)) > 1
  
      else:
          # if none of the preceding rules returned true, then it is not an endpoint
          return False