Squish-unsquish ligatures
JavaScript (ES6), 213 bytes - 20% bonus = 170.4

s=&gt;eval('for(p=o="";m=s.match(r="ﬄ|ﬃ|ﬂ|ﬁ|ﬀ|Ĳ|ĳ|Œ|œ|Æ|æ|ffl|ffi|fl|fi|ff|IJ|ij|OE|oe|AE|ae",x=r.split`|`);s=s.slice(i+t.length-(p=t&lt;"z")))o+=s.slice(p,i=m.index)+x[(x.indexOf(t=m[0])+11)%22];o+s.slice(p)')


Explanation

s=&gt;                           // s = input string
  eval(`                      // use eval to avoid writing {} or return
    for(                      // iterate over each ligature match
      p=                      // p = 1 if the last match was a non-unicode ligature
        o="";                 // o = output string
      m=s.match(              // find the next ligature

        // r = regex string for ligatures (unicode and non-unicode)
        r="ﬄ|ﬃ|ﬂ|ﬁ|ﬀ|Ĳ|ĳ|Œ|œ|Æ|æ|ffl|ffi|fl|fi|ff|IJ|ij|OE|oe|AE|ae",
        x=r.split\`|\`        // x = arrray of r

      );
      s=s.slice(i+t.length    // remove the part that has been added to the output
        -(p=t&lt;"z"))           // if we matched a non-unicode ligature, keep the last
    )                         //     character so it can be part of the next match
      o+=s.slice(p,i=m.index) // add the text before the match to the output
        +x[(x.indexOf(        // add the opposite type of the matched ligature
          t=m[0]              // t = matched text
        )+11)%22];            // (index + 11) % 22 returns the opposite index
    o+s.slice(p)              // return o + any remaining characters
  `)


Test



var solution = s=&gt;eval('for(p=o="";m=s.match(r="ﬄ|ﬃ|ﬂ|ﬁ|ﬀ|Ĳ|ĳ|Œ|œ|Æ|æ|ffl|ffi|fl|fi|ff|IJ|ij|OE|oe|AE|ae",x=r.split`|`);s=s.slice(i+t.length-(p=t&lt;"z")))o+=s.slice(p,i=m.index)+x[(x.indexOf(t=m[0])+11)%22];o+s.slice(p)')
&lt;input type="text" id="input" value="ﬀiffffij" oninput="result.textContent=solution(input.value)" /&gt;
&lt;pre id="result"&gt;&lt;/pre&gt;