class AbstractWordCoherencyRule extends TextLevelRule {

  /**
   * Maps words in both directions, e.g. "aufwendig -&gt; aufwändig" and "aufwändig -&gt; aufwendig".
   * @since 3.0
   */
  protected abstract Map<String, Set<String>> getWordMap();

  /**
   * Get the message shown to the user if the rule matches.
   */
  protected abstract String getMessage(String word1, String word2);

  protected String getShortMessage() {
    return null;
  };
  
  public AbstractWordCoherencyRule(ResourceBundle messages) throws IOException {
    super.setCategory(Categories.MISC.getCategory(messages));
  }
  
  @Override
  public RuleMatch[] match(List<AnalyzedSentence> sentences) {<FILL_FUNCTION_BODY>}

  @Override
  public int minToCheckParagraph() {
    return -1;
  }

  protected String createReplacement(String marked, String token, String otherSpelling, AnalyzedTokenReadings tmpToken) {
    return  marked.replaceFirst("(?i)" + token, otherSpelling);
  }

}

    List<RuleMatch> ruleMatches = new ArrayList<>();
    Map<String, String> shouldNotAppearWord = new HashMap<>();  // e.g. aufwändig -> aufwendig
    int pos = 0;
    for (AnalyzedSentence sentence : sentences) {
      AnalyzedTokenReadings[] tokens = sentence.getTokensWithoutWhitespace();
      for (AnalyzedTokenReadings tmpToken : tokens) {
        String token = tmpToken.getToken();
        List<AnalyzedToken> readings = tmpToken.getReadings();
        if (!readings.isEmpty()) {
          Set<String> baseforms = readings.stream().map(AnalyzedToken::getLemma).collect(Collectors.toSet());
          for (String baseform : baseforms) {
            if (baseform != null) {
              token = baseform;
            }
            int fromPos = pos + tmpToken.getStartPos();
            int toPos = pos + tmpToken.getEndPos();
            if (shouldNotAppearWord.containsKey(token)) {
              String otherSpelling = shouldNotAppearWord.get(token);
              String msg = getMessage(token, otherSpelling);
              RuleMatch ruleMatch = new RuleMatch(this, sentence, fromPos, toPos, msg);
              String marked = sentence.getText().substring(tmpToken.getStartPos(), tmpToken.getEndPos());
              String replacement = createReplacement(marked, token, otherSpelling, tmpToken);
              if (StringTools.startsWithUppercase(tmpToken.getToken())) {
                replacement = StringTools.uppercaseFirstChar(replacement);
              }
              if (!marked.equalsIgnoreCase(replacement)) {   // see https://github.com/languagetool-org/languagetool/issues/3493
                ruleMatch.setSuggestedReplacement(replacement);
                ruleMatches.add(ruleMatch);
              }
              ruleMatch.setShortMessage(getShortMessage());
              break;
            } else if (getWordMap().containsKey(token)) {
              Set<String> shouldNotAppearSet = getWordMap().get(token);
              for (String shouldNotAppear : shouldNotAppearSet) {
                shouldNotAppearWord.put(shouldNotAppear, token);
              }
            }
          }
        }
      }
      pos += sentence.getCorrectedTextLength();
    }
    return toRuleMatchArray(ruleMatches);
  
<methods>public void <init>() ,public void <init>(java.util.ResourceBundle) ,public int estimateContextForSureMatch() ,public org.languagetool.rules.RuleMatch[] match(List<org.languagetool.AnalyzedSentence>, org.languagetool.markup.AnnotatedText) throws java.io.IOException,public abstract org.languagetool.rules.RuleMatch[] match(List<org.languagetool.AnalyzedSentence>) throws java.io.IOException,public final org.languagetool.rules.RuleMatch[] match(org.languagetool.AnalyzedSentence) throws java.io.IOException,public abstract int minToCheckParagraph() <variables>