code: def _range_indices(
      df: pd.DataFrame,
      right: pd.DataFrame,
      first: tuple,
      second: tuple,
  ):
      """
      Retrieve index positions for range/interval joins.
  
      Idea inspired by article:
      https://www.vertica.com/blog/what-is-a-range-join-and-why-is-it-so-fastba-p223413/
  
      Returns a tuple of (left_index, right_index)
      """
      # summary of code for range join:
      # get the positions where start_left is >/>= start_right
      # then within the positions,
      # get the positions where end_left is </<= end_right
      # this should reduce the search space
  
      left_on, right_on, op = first
      left_c = df[left_on]
      right_c = right[right_on]
      left_on, right_on, _ = second
      # get rid of any nulls
      # this is helpful as we can convert extension arrays
      # to numpy arrays safely
      # and simplify the search logic below
      any_nulls = df[left_on].isna()
      if any_nulls.any():
          left_c = left_c[~any_nulls]
      any_nulls = right[right_on].isna()
      if any_nulls.any():
          right_c = right_c[~any_nulls]
  
      outcome = _generic_func_cond_join(
          left=left_c,
          right=right_c,
          op=op,
          multiple_conditions=True,
          keep="all",
      )
  
      if outcome is None:
          return None
  
      left_index, right_index, search_indices = outcome
      left_on, right_on, op = second
      right_c = right.loc[right_index, right_on]
      left_c = df.loc[left_index, left_on]
      # if True, we can use a binary search
      # for more performance, instead of a linear search
      fastpath = right_c.is_monotonic_increasing
      if fastpath:
          outcome = _generic_func_cond_join(
              left=left_c,
              right=right_c,
              op=op,
              multiple_conditions=False,
              keep="first",
          )
          if outcome is None:
              return None
          left_c, pos = outcome
      else:
          # the aim here is to get the first match
          # where the left array is </<= than the right array
          # this is solved by getting the cumulative max
          # thus ensuring that the first match is obtained
          # via a binary search
          # this allows us to avoid the less efficient linear search
          # of using a for loop with a break to get the first match
          outcome = _generic_func_cond_join(
              left=left_c,
              right=right_c.cummax(),
              op=op,
              multiple_conditions=True,
              keep="all",
          )
          if outcome is None:
              return None
          left_c, right_index, pos = outcome
      if left_c.size < left_index.size:
          keep_rows = np.isin(left_index, left_c, assume_unique=True)
          search_indices = search_indices[keep_rows]
          left_index = left_c
      # no point searching within (a, b)
      # if a == b
      # since range(a, b) yields none
      keep_rows = pos < search_indices
  
      if not keep_rows.any():
          return None
  
      if not keep_rows.all():
          left_index = left_index[keep_rows]
          pos = pos[keep_rows]
          search_indices = search_indices[keep_rows]
  
      repeater = search_indices - pos
      if (repeater == 1).all():
          # no point running a comparison op
          # if the width is all 1
          # this also implies that the intervals
          # do not overlap on the right side
          return left_index, right_index[pos]
  
      right_index = [
          right_index[start:end]
          for start, end in zip(pos, search_indices)
      ]
  
      right_index = np.concatenate(right_index)
      left_index = np.repeat(left_index, repeater)
  
      if fastpath:
          return left_index, right_index
      # here we search for actual positions
      # where left_c is </<= right_c
      # safe to index the arrays, since we are picking the positions
      # which are all in the original `df` and `right`
      # doing this allows some speed gains
      # while still ensuring correctness
      left_c = df[left_on]._values[left_index]
      right_c = right[right_on]._values[right_index]
      ext_arr = is_extension_array_dtype(left_c)
      op = operator_map[op]
      mask = op(left_c, right_c)
  
      if ext_arr:
          mask = mask.to_numpy(dtype=bool, na_value=False)
  
      if not mask.all():
          left_index = left_index[mask]
          right_index = right_index[mask]
  
      return left_index, right_index
  
imports: from __future__ import annotations
  import operator
  import warnings
  from typing import Any, Hashable, Literal, Optional, Union
  import numpy
  import pandas
  import pandas_flavor
  from pandas.api.types import is_datetime64_dtype, is_dtype_equal, is_extension_array_dtype, is_numeric_dtype, is_timedelta64_dtype
  from pandas.core.reshape.merge import _MergeOperation
  from janitor.functions.utils import _generic_func_cond_join, _JoinOperator, _keep_output, col, greater_than_join_types, less_than_join_types
  from janitor.utils import check, check_column, find_stack_level
label_desc: Use image node_id 10 for calling a global function with example usage: _range_indices(df, right, first, second) and returns: left_index,  right_index, None, None, left_index,  right_index, left_index,  right_index, None, None