Example sorted array:
0 1 2 3
1 5 5 7
There are four cases:
(Direction: go to higher or lower neighbor on non-match?)
(Duplicates: only for lower_bound, upper_bound never matches on equality)
Some observations:
last_lower_bound(value):
  (lb,ub) = equal_range(value)
  if (lb!=ub):
    return ub-1
  return lb    # or: return ub
    
Especially the reverse_lower_bound function is sometimes needed, but the STL does not provide it (well, you can use greater instead of less...):
reverse_lower_bound(value):
  (lb,ub) = equal_range(value)
  if (lb!=ub):
    return lb
  return lb-1
    
Let's look into in tree-based implementations:
lower_bound(key):
  var node=tree.root, ret=null
  while (node):
    if (key<=node.key):    # upper_bound has < here
      ret=node             # move this to the else branch to get inverse_upper_bound/inverse_lower_bound
      node=node.left
    else:
      node=node.right
  return ret
    
last_lower_bound(key):
  var node=tree.root, ret=null
  while (node):
    if (key==node.key):
      ret=node            # do this, if you want to include the value (i.e. lower_bound)
      node=node.right     # left gives you the lowest index duplicate, right the highest
                          # (if you don't include the value, .left/.right must be used for reverse/forward (?))
    else if (key<node.key):
      ret=node            # direction: forward (after else: reverse)
      node=node.left
    else:
      node=node.right
  return ret