<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:

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