How do I handle smart quotes and curly apostrophes?
Converting to straight quotes (and using Typogrify to render them as
smart quotes) would be fine too, if you can tell me how to convert to
straight quotes.

I had to do that for an online dictionary I built recently with Drupal 9.2. What I did is to create a drush command that updates all nodes (creating a new revision) converting all quote characters to straight quotes or ASCII quotes:
&lt;?php

namespace Drupal\diccionari_custom\Commands;

use Drush\Commands\DrushCommands;

/**
 * A Drush commandfile.
 */
class DiccionariCustomCommands extends DrushCommands {

  /**
   * Standarize quotes.
   *
   * @usage diccionari_custom:standarizeQuotes
   *   Usage description
   *
   * @command diccionari_custom:standarizeQuotes
   */
  public function standarizeQuotes() {

    $query = \Drupal::entityQuery('node');
    $entity_ids = $query-&gt;execute();
    foreach ($entity_ids as $nid) {
      $node = \Drupal\node\Entity\Node::load($nid);

      $new_title = str_replace('‘', &quot;'&quot;, $node-&gt;title-&gt;value);
      $new_title = str_replace('’', &quot;'&quot;, $new_title);
      $new_title = str_replace('ʹ', &quot;'&quot;, $new_title);
      $new_title = str_replace('ꞌ', &quot;'&quot;, $new_title);
      $new_title = str_replace('“', '&quot;', $new_title);
      $new_title = str_replace('”', '&quot;', $new_title);
      $new_title = str_replace('&amp;#x2018;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#x2019;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#x02B9;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#xA78C;', &quot;'&quot;, $new_title);
      $new_title = str_replace('&amp;#x201C;', '&quot;', $new_title);
      $new_title = str_replace('&amp;#x201D;', '&quot;', $new_title);
      $node-&gt;setTitle($new_title);

      $new_content = str_replace('‘', &quot;'&quot;, $node-&gt;body-&gt;value);
      $new_content = str_replace('’', &quot;'&quot;, $new_content);
      $new_content = str_replace('ʹ', &quot;'&quot;, $new_content);
      $new_content = str_replace('ꞌ', &quot;'&quot;, $new_content);
      $new_content = str_replace('“', '&quot;', $new_content);
      $new_content = str_replace('”', '&quot;', $new_content);
      $new_content = str_replace('&amp;#x2018;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#x2019;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#x02B9;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#xA78C;', &quot;'&quot;, $new_content);
      $new_content = str_replace('&amp;#x201C;', '&quot;', $new_content);
      $new_content = str_replace('&amp;#x201D;', '&quot;', $new_content);
      $node-&gt;body-&gt;value  = $new_content;
      $node-&gt;body-&gt;format = 'basic_html';

      $node-&gt;setNewRevision(TRUE);
      $node-&gt;revision_log = 'Converting quotes to straight quotes';
      $node-&gt;setRevisionCreationTime(REQUEST_TIME);
      $node-&gt;setRevisionUserId(1);
      $node-&gt;save();

      $this-&gt;logger()-&gt;success(dt($nid . ':' . $node-&gt;title-&gt;value));
    }
  }
}

Now, when outputting the content, you can use a text format and Typogrify module to do the conversion to smart quotes.
And if you have to perform that conversion in the title too, see my last comment in this Drupal.org issue.

Preferably your solution will handle other characters as well.

Both the drush command example and Typogrify settings can easily be extended to handle other characters.