docs: def find_soft_selectors(ids_and_clean_visible, start_num_tokens='10',
                          max_num_tokens='20', filter_punctuation='0'):
      '''External interface for dossier.models.soft_selectors.
  
      This at scans through `num_tokens` values between
      `start_num_tokens` and `max_num_tokens` and calls
      `find_soft_selectors_at_n` looking for results
  
      All of the params can be passed from URL parameters, in which
      case they can be strings and this function will type cast them
      appropriately.
      '''
      start_num_tokens = int(start_num_tokens)
      max_num_tokens = int(max_num_tokens)
      filter_punctuation = bool(int(filter_punctuation))
  
      if not ids_and_clean_visible:
          logger.info('find_soft_selectors called with no ids_and_clean_visible')
          return []
  
      current_results = [] ## results from current n
      previous_results = [] ## previous results from last n
      overall_results = [] ## overall results to return
  
      for num_tokens in range(start_num_tokens, max_num_tokens + 1):
  
          ## update this here
          previous_results = current_results
  
          results_at_n = find_soft_selectors_at_n(
              ids_and_clean_visible, num_tokens, filter_punctuation)
          if len(results_at_n) == 0:
              break
  
          best_score = results_at_n[0]['score']
  
          ## i.e. the initial condition is they all have the same score
          idx_at_second = len(results_at_n)
  
          for idx, result in enumerate(results_at_n):
              if result['score'] < best_score:
                  idx_at_second = idx
                  break
  
          current_results = results_at_n[0:idx_at_second]
  
          if num_tokens == 8:
              for r in results_at_n:
                  logger.info('%s --- score: %d' % (r['phrase'], r['score']))
  
          if previous_results == []:
              logger.info('Previous results are empty. Continuing.')
              continue
  
          ## now, the main idea is to figure out if any strings from previous
          ## are substrings of those from current
          ## (with the scores fixed at the max for that subphrase).
          ## when they stop being substrings
          ## then those are completed phrases and should be returned as a result
  
          for prev_result in previous_results:
  
              is_subbed_and_same_score = False
  
              for curr_result in current_results:
                  if prev_result['phrase'] in curr_result['phrase'] and \
                      prev_result['score'] == curr_result['score'] :
  
                      is_subbed_and_same_score = True
                      break
  
              if not is_subbed_and_same_score: ## then it's a honest result
                  prev_result['n'] = num_tokens - 1
  
                  overall_results.append(prev_result)
  
          if len(current_results) == 0:
              ## we got them all
              ## (we still had to collect the previous results)
              ## that's why this break comes after the previous for loop
              break
  
  
      ## also add results from current_results at final n
      for result in current_results:
          result['n'] = num_tokens
          overall_results.append(result)
  
      ## sort by score then by length
      overall_results.sort(key=itemgetter('score', 'n'), reverse=True)
  
      logger.info('OVERALL RESULTS: %d' % len(overall_results))
      # for idx, result in enumerate(overall_results):
          # logger.info('%d. %s --- score: %f , n = %d, hits=%d' %
              # (idx, result['phrase'], result['score'], result['n'], len(result['hits']))
          # )
      return overall_results