class MatchesForReordering extends RemoteRequest {
    final List<AnalyzedSentence> sentences;
    final List<RuleMatch> matches;
    final List<RemoteLanguageModel.Request> requests;
    MatchesForReordering(List<AnalyzedSentence> sentences, List<RuleMatch> matches, List<RemoteLanguageModel.Request> requests) {
      this.sentences = sentences;
      this.matches = matches;
      this.requests = requests;
    }
  }

  /**
   * transform suggestions before resorting, e.g. limit resorting to top-n candidates
   * @return transformed suggestions
   */
  protected List<SuggestedReplacement> prepareSuggestions(List<SuggestedReplacement> suggestions) {
    // include more suggestions for resorting if there are translations included as original order isn't that good
    if (suggestions.stream().anyMatch(s -> s.getType() == SuggestedReplacement.SuggestionType.Translation)) {
      suggestionLimit = 25;
    } else {
      suggestionLimit = 10;
    }
    return suggestions.subList(0, Math.min(suggestions.size(), suggestionLimit));
  }

  private static final int MIN_WORDS = 8;
  private static final double MAX_ERROR_RATE = 0.5;

  @Override
  protected RemoteRequest prepareRequest(List<AnalyzedSentence> sentences, Long textSessionId) {
    List<RuleMatch> matches = new LinkedList<>();
    int totalWords = 0;
    try {
      for (AnalyzedSentence sentence : sentences) {
        RuleMatch[] sentenceMatches = wrappedRule.match(sentence);
        Collections.addAll(matches, sentenceMatches);

        // computing many suggestions (e.g. for requests with the language incorrectly set, or with garbled input) is very expensive
        // having this as a RemoteRule circumvents the normal ErrorRateTooHighException
        // so build this logic again here
        int words = sentence.getTokensWithoutWhitespace().length;
        totalWords += words;
        if (words > MIN_WORDS && (double) sentenceMatches.length / words > MAX_ERROR_RATE) {
          for (RuleMatch m : sentenceMatches) {
            m.discardLazySuggestedReplacements();
          }
          logger.info("Skipping suggestion generation for sentence, too many matches ({} matches in {} words)",
            sentenceMatches.length, words);
        }
      }
    } catch (IOException e) {
      logger.error("Error while executing rule " + wrappedRule.getId(), e);
      return new MatchesForReordering(sentences, Collections.emptyList(), Collections.emptyList());
    }
    if (totalWords > MIN_WORDS && (double) matches.size() / totalWords > MAX_ERROR_RATE) {
      logger.info("Skipping suggestion generation for request, too many matches ({} matches in {} words)",
        matches.size(), totalWords);
      matches.forEach(RuleMatch::discardLazySuggestedReplacements);
      return new MatchesForReordering(sentences, matches, Collections.emptyList());
    }
    List<RemoteLanguageModel.Request> requests = new LinkedList<>();
    for (RuleMatch match : matches) {
      match.setSuggestedReplacementObjects(prepareSuggestions(match.getSuggestedReplacementObjects()));
      requests.add(buildRequest(match));
    }
    return new MatchesForReordering(sentences, matches, requests);
  }

  @Override
  protected RemoteRuleResult fallbackResults(RemoteRequest request) {
    MatchesForReordering req = (MatchesForReordering) request;
    return new RemoteRuleResult(false, false, req.matches, req.sentences);
  }

  @Override
  protected Callable<RemoteRuleResult> executeRequest(RemoteRequest request, long timeoutMilliseconds) throws TimeoutException {
    return () -> {
      if (model == null) {
        return fallbackResults(request);
      }
      MatchesForReordering data = (MatchesForReordering) request;
      List<RuleMatch> matches = data.matches;
      List<RemoteLanguageModel.Request> requests = data.requests;
      Streams.FunctionWithIndex<RemoteLanguageModel.Request, Long> mapIndices = (req, index) -> req != null ? index : null;
      List<Long> indices = Streams.mapWithIndex(requests.stream(), mapIndices)
        .filter(Objects::nonNull).collect(Collectors.toList());
      requests = requests.stream().filter(Objects::nonNull).collect(Collectors.toList());

      if (requests.isEmpty()) {
        return new RemoteRuleResult(false, true, matches, data.sentences);
      } else {
        List<List<Double>> results = model.batchScore(requests, timeoutMilliseconds);
        // put curated at the top, then compare probabilities
        for (int i = 0; i < indices.size(); i++) {
          List<Double> scores = results.get(i);
          String userWord = requests.get(i).text.substring(requests.get(i).start, requests.get(i).end);
          RuleMatch match = matches.get(indices.get(i).intValue());
          //RemoteLanguageModel.Request req = requests.get(i);
          //String error = req.text.substring(req.start, req.end);
          //logger.info("Scored suggestions for '{}': {} -> {}", error, match.getSuggestedReplacements(), Streams
          //  .zip(match.getSuggestedReplacementObjects().stream(), scores.stream(), Pair::of)
          //  .sorted(new CuratedAndSameCaseComparator(userWord))
          //  .map(scored -> String.format("%s (%e)", scored.getLeft().getReplacement(), scored.getRight()))
          //  .collect(Collectors.toList()));
          List<SuggestedReplacement> ranked = Streams
            .zip(match.getSuggestedReplacementObjects().stream(), scores.stream(), Pair::of)
            .sorted(new CuratedAndSameCaseComparator(userWord))
            .map(Pair::getLeft)
            .collect(Collectors.toList());
          //logger.info("Reordered correction for '{}' from {} to {}", error, req.candidates, ranked);
          match.setSuggestedReplacementObjects(ranked);
        }
        return new RemoteRuleResult(true, true, matches, data.sentences);
      }
    };
  }

  @Nullable
  private RemoteLanguageModel.Request buildRequest(RuleMatch match) {<FILL_FUNCTION_BODY>

    List<String> suggestions = match.getSuggestedReplacements();
    if (suggestions != null && suggestions.size() > 1) {
      return new RemoteLanguageModel.Request(
        match.getSentence().getText(), match.getFromPos(), match.getToPos(), suggestions);
    } else {
      return null;
    }
  
<methods>public void <init>(org.languagetool.Language, java.util.ResourceBundle, org.languagetool.rules.RemoteRuleConfig, boolean, java.lang.String) ,public void <init>(org.languagetool.Language, java.util.ResourceBundle, org.languagetool.rules.RemoteRuleConfig, boolean) ,public CircuitBreaker circuitBreaker() ,public static void fixMatchOffsets(org.languagetool.AnalyzedSentence, List<org.languagetool.rules.RuleMatch>) ,public java.lang.String getId() ,public org.languagetool.rules.RemoteRuleConfig getServiceConfiguration() ,public long getTimeout(long) ,public boolean isPremium() ,public org.languagetool.rules.RuleMatch[] match(org.languagetool.AnalyzedSentence) throws java.io.IOException,public FutureTask<org.languagetool.rules.RemoteRuleResult> run(List<org.languagetool.AnalyzedSentence>) ,public FutureTask<org.languagetool.rules.RemoteRuleResult> run(List<org.languagetool.AnalyzedSentence>, java.lang.Long) ,public static void shutdown() <variables>protected static final ConcurrentMap<java.lang.String,CircuitBreaker> circuitBreakers,protected final non-sealed boolean filterMatches,protected final non-sealed boolean fixOffsets,protected final non-sealed boolean inputLogging,private static final Logger logger,protected final non-sealed org.languagetool.JLanguageTool lt,protected final non-sealed boolean premium,protected final non-sealed org.languagetool.Language ruleLanguage,protected final non-sealed org.languagetool.rules.RemoteRuleConfig serviceConfiguration,protected static final List<java.lang.Runnable> shutdownRoutines,protected final non-sealed java.util.regex.Pattern suppressMisspelledMatch,protected final non-sealed java.util.regex.Pattern suppressMisspelledSuggestions,protected final non-sealed boolean whitespaceNormalisation