Repository: ashvardanian/StringZilla (language: rust)

Issue:
Bug:  compilation error on `partition` with single char argument
### Describe the bug

I met compilation error on following code in README.md

```cpp
auto parts = haystack.partition(':'); // Matching a character
```

It seem to be caused by the lack of overload function definition following:
```
partition_type partition(char pattern) const noexcept { return partition_(char_set{pattern}, 1); }
```


### Steps to reproduce

```cpp
#include "stringzilla/stringzilla.hpp"

int main() {
  auto haystack = ashvardanian::stringzilla::string_view{"a:b:c"};
  auto [before, match, after] = haystack.partition(':');
}
```

OS: Fedora Linux 40


### Expected behavior

Same as `haystack.partition(ashvardanian::stringzilla::char_set(";"))`


### StringZilla version

3.9.6

### Operating System

Fedora Linux 40

### Hardware architecture

x86

### Which interface are you using?

C++ bindings

### Contact Details

_No response_

### Are you open to being tagged as a contributor?

- [X] I am open to being mentioned in the project `.git` history as a contributor

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:
Thank you, @toge!
If it’s missing, it’s worth adding, but should use the substring search under the hood, as opposed to character-set search. 
@ashvardanian 
Thank you for your advice!
I understand the solution you suggested.

Could you update examples in README.md?
I think we should avoid a situation where someone tries to compile the sample code and it fails.


Fix patch:
diff --git a/.github/workflows/build_tools.sh b/.github/workflows/build_tools.sh
deleted file mode 100755
index f6a9dc63..00000000
--- a/.github/workflows/build_tools.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-
-# Assign arguments to variables
-BUILD_TYPE=$1    # Debug or Release
-COMPILER=$2      # GCC, LLVM, or MSVC
-
-# Set common flags
-COMMON_FLAGS="-DSTRINGZILLA_BUILD_TEST=1 -DSTRINGZILLA_BUILD_BENCHMARK=1 -DSTRINGZILLA_BUILD_SHARED=0"
-
-# Compiler specific settings
-case "$COMPILER" in
-    "GCC")
-        COMPILER_FLAGS="-DCMAKE_CXX_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++"
-        ;;
-    "LLVM")
-        COMPILER_FLAGS="-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++"
-        ;;
-    "MSVC")
-        COMPILER_FLAGS="" 
-        ;;
-    *)
-        echo "Unknown compiler: $COMPILER"
-        exit 1
-        ;;
-esac
-
-# Set build type
-case "$BUILD_TYPE" in
-    "Debug")
-        BUILD_DIR="build_debug"
-        BUILD_FLAGS="-DCMAKE_BUILD_TYPE=Debug"
-        ;;
-    "Release")
-        BUILD_DIR="build_release"
-        BUILD_FLAGS="-DCMAKE_BUILD_TYPE=RelWithDebInfo"
-        ;;
-    *)
-        echo "Unknown build type: $BUILD_TYPE"
-        exit 1
-        ;;
-esac
-
-# Execute commands
-cmake $COMMON_FLAGS $COMPILER_FLAGS $BUILD_FLAGS -B $BUILD_DIR && cmake --build $BUILD_DIR --config $BUILD_TYPE
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index 94d6928c..3fbe9b44 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -20,7 +20,7 @@ permissions:
 jobs:
   versioning:
     name: Update Version
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -100,7 +100,7 @@ jobs:
 
         # Python
       - name: Set up Python ${{ env.PYTHON_VERSION }}
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v5.2.0
         with:
           python-version: ${{ env.PYTHON_VERSION }}
       - name: Build Python
@@ -184,7 +184,7 @@ jobs:
 
         # Python
       - name: Set up Python ${{ env.PYTHON_VERSION }}
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v5.2.0
         with:
           python-version: ${{ env.PYTHON_VERSION }}
       - name: Build Python
@@ -281,7 +281,7 @@ jobs:
 
   test_macos:
     name: MacOS
-    runs-on: macos-latest
+    runs-on: macos-13
 
     steps:
       - uses: actions/checkout@v4
@@ -316,7 +316,7 @@ jobs:
 
         # Python
       - name: Set up Python ${{ env.PYTHON_VERSION }}
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v5.2.0
         with:
           python-version: ${{ env.PYTHON_VERSION }}
       - name: Build Python
@@ -324,6 +324,8 @@ jobs:
           python -m pip install --upgrade pip
           pip install pytest pytest-repeat numpy pyarrow
           python -m pip install .
+        env:
+          MACOSX_DEPLOYMENT_TARGET: "11.0"
       - name: Test Python
         run: pytest scripts/test.py -s -x
 
@@ -346,7 +348,7 @@ jobs:
 
   test_windows:
     name: Windows
-    runs-on: windows-latest
+    runs-on: windows-2022
     steps:
       - uses: actions/checkout@v4
       - uses: ilammy/msvc-dev-cmd@v1
@@ -387,7 +389,7 @@ jobs:
 
         # Python
       - name: Set up Python ${{ env.PYTHON_VERSION }}
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v5.2.0
         with:
           python-version: ${{ env.PYTHON_VERSION }}
       - name: Build Python
@@ -400,7 +402,7 @@ jobs:
 
   test_alpine:
     name: Alpine Linux
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24
     container:
       image: alpine:latest
       options: --privileged # If needed for certain Docker operations
@@ -449,18 +451,18 @@ jobs:
       ]
     strategy:
       matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
+        os: [ubuntu-24, macos-13, windows-2022]
         python-version: ["36", "37", "38", "39", "310", "311", "312"]
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v5.2.0
         with:
           python-version: 3.x
 
         # We only need QEMU for Linux builds
       - name: Setup QEMU
-        if: matrix.os == 'ubuntu-latest'
+        if: matrix.os == 'ubuntu-24'
         uses: docker/setup-qemu-action@v3
       - name: Install cibuildwheel
         run: python -m pip install cibuildwheel
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a6767394..144ae8b0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ permissions:
 jobs:
   versioning:
     name: Update Version
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -49,7 +49,7 @@ jobs:
 
   rebase:
     name: Rebase Dev. Branch
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24
     if: github.ref == 'refs/heads/main'
     needs: versioning
     steps:
@@ -78,7 +78,7 @@ jobs:
     needs: versioning
     strategy:
       matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
+        os: [ubuntu-24, macos-13, windows-2022]
         python-version: ["36", "37", "38", "39", "310", "311", "312"]
     steps:
       - uses: actions/checkout@v4
@@ -86,11 +86,11 @@ jobs:
           ref: "main"
 
       - name: Set up Python
-        uses: actions/setup-python@v5
+        uses: actions/setup-python@v5.2.0
         with:
           python-version: 3.x
       - name: Setup QEMU
-        if: matrix.os == 'ubuntu-latest' # We only need QEMU for Linux builds
+        if: matrix.os == 'ubuntu-24' # We only need QEMU for Linux builds
         uses: docker/setup-qemu-action@v3
       - name: Install cibuildwheel
         run: python -m pip install cibuildwheel
@@ -98,6 +98,7 @@ jobs:
         run: cibuildwheel --output-dir wheelhouse
         env:
           CIBW_BUILD: cp${{ matrix.python-version }}-*
+          MACOSX_DEPLOYMENT_TARGET: "11.0"
       - name: Upload wheels
         uses: actions/upload-artifact@v4
         with:
@@ -152,7 +153,7 @@ jobs:
   # publish_javascript:
   #   name: Publish JavaScript
   #   needs: versioning
-  #   runs-on: ubuntu-latest
+  #   runs-on: ubuntu-24
   #   steps:
   #     - uses: actions/checkout@v4
   #       with:
@@ -297,7 +298,7 @@ jobs:
 
   create_macos_library:
     name: Create Library for MacOS ${{ matrix.arch }}
-    runs-on: macos-latest
+    runs-on: macos-13
     needs: versioning
     strategy:
       fail-fast: false
diff --git a/.gitignore b/.gitignore
index 6fd5cd1b..58e1c789 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,7 @@ node_modules/
 leipzig1M.txt
 enwik9.txt
 xlsum.csv
+human_protein_1200row_800len.txt
+
+# StringZilla-specific log files
+/failed_sz_*
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index d899d3fe..71d59186 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -8,6 +8,7 @@
       "name": "Debug C++ Unit Tests",
       "type": "cppdbg",
       "request": "launch",
+      "preLaunchTask": "Build Test: Debug",
       "program": "${workspaceFolder}/build_debug/stringzilla_test_cpp20",
       "cwd": "${workspaceFolder}",
       "environment": [
@@ -18,16 +19,13 @@
       ],
       "stopAtEntry": false,
       "linux": {
-        "preLaunchTask": "Build with GCC: Debug",
         "MIMode": "gdb"
       },
       "osx": {
-        "preLaunchTask": "Build with LLVM: Debug",
         "MIMode": "lldb"
       },
       "windows": {
         "program": "${workspaceFolder}\\build_debug\\stringzilla_test_cpp20.exe",
-        "preLaunchTask": "Build with MSVC: Debug",
         "MIMode": "gdb",
         "miDebuggerPath": "C:\\MinGw\\bin\\gdb.exe"
       }
@@ -48,17 +46,15 @@
         }
       ],
       "stopAtEntry": false,
+      "preLaunchTask": "Build Benchmarks: Debug",
       "linux": {
-        "preLaunchTask": "Build with GCC: Debug",
         "MIMode": "gdb"
       },
       "osx": {
-        "preLaunchTask": "Build with LLVM: Debug",
         "MIMode": "lldb"
       },
       "windows": {
         "program": "${workspaceFolder}\\build_debug\\stringzilla_${fileBasenameNoExtension}.exe",
-        "preLaunchTask": "Build with MSVC: Debug",
         "MIMode": "gdb",
         "miDebuggerPath": "C:\\MinGw\\bin\\gdb.exe"
       }
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c3c54adc..ee77189d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -257,7 +257,8 @@
     "xtr1common": "cpp",
     "xtree": "cpp",
     "xutility": "cpp",
-    "errno.h": "c"
+    "errno.h": "c",
+    "text_encoding": "cpp"
   },
   "python.pythonPath": "~/miniconda3/bin/python"
 }
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index dfb3e46d..1ffc6a28 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -2,40 +2,58 @@
     "version": "2.0.0",
     "tasks": [
         {
-            "label": "Build with GCC: Debug",
-            "command": "./.github/workflows/build_tools.sh Debug GCC",
+            "label": "Build Test: Debug",
+            "command": "cmake -D CMAKE_BUILD_TYPE=Debug -D STRINGZILLA_BUILD_TEST=1 -B build_debug && cmake --build build_debug --config Debug --target stringzilla_test_cpp20",
+            "args": [],
             "type": "shell",
-            "problemMatcher": [
-                "$gcc"
-            ]
+            "osx": {
+                "environment": [
+                    {
+                        "name": "CXX",
+                        "value": "$(brew --prefix llvm)/bin/clang++"
+                    },
+                    {
+                        "name": "CC",
+                        "value": "$(brew --prefix llvm)/bin/clang"
+                    }
+                ]
+            }
         },
         {
-            "label": "Build with GCC: Release",
-            "command": "./.github/workflows/build_tools.sh Release GCC",
+            "label": "Build Benchmarks: Debug",
+            "command": "cmake -D CMAKE_BUILD_TYPE=Debug -D STRINGZILLA_BUILD_TEST=0 -D STRINGZILLA_BUILD_BENCHMARK=1 -B build_debug && cmake --build build_debug --config Debug",
+            "args": [],
             "type": "shell",
-            "problemMatcher": [
-                "$gcc"
-            ]
+            "osx": {
+                "environment": [
+                    {
+                        "name": "CXX",
+                        "value": "$(brew --prefix llvm)/bin/clang++"
+                    },
+                    {
+                        "name": "CC",
+                        "value": "$(brew --prefix llvm)/bin/clang"
+                    }
+                ]
+            }
         },
         {
-            "label": "Build with LLVM: Debug",
-            "command": "./.github/workflows/build_tools.sh Debug LLVM",
-            "type": "shell"
-        },
-        {
-            "label": "Build with LLVM: Release",
-            "command": "./.github/workflows/build_tools.sh Release LLVM",
-            "type": "shell"
-        },
-        {
-            "label": "Build with MSVC: Debug",
-            "command": "./.github/workflows/build_tools.sh Debug MSVC",
-            "type": "shell"
+            "label": "Build Benchmarks: Release",
+            "command": "cmake -D CMAKE_BUILD_TYPE=Release -D STRINGZILLA_BUILD_TEST=0 -D STRINGZILLA_BUILD_BENCHMARK=1 -B build_release && cmake --build build_release --config Release",
+            "args": [],
+            "type": "shell",
+            "osx": {
+                "environment": [
+                    {
+                        "name": "CXX",
+                        "value": "$(brew --prefix llvm)/bin/clang++"
+                    },
+                    {
+                        "name": "CC",
+                        "value": "$(brew --prefix llvm)/bin/clang"
+                    }
+                ]
+            }
         },
-        {
-            "label": "Build with MSVC: Release",
-            "command": "./.github/workflows/build_tools.sh Release MSVC",
-            "type": "shell"
-        }
     ]
 }
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8549ee32..be6d9dcd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -264,6 +264,7 @@ if(${STRINGZILLA_BUILD_BENCHMARK})
   define_launcher(stringzilla_bench_sort scripts/bench_sort.cpp 17 "${STRINGZILLA_TARGET_ARCH}")
   define_launcher(stringzilla_bench_token scripts/bench_token.cpp 17 "${STRINGZILLA_TARGET_ARCH}")
   define_launcher(stringzilla_bench_container scripts/bench_container.cpp 17 "${STRINGZILLA_TARGET_ARCH}")
+  define_launcher(stringzilla_bench_memory scripts/bench_memory.cpp 17 "${STRINGZILLA_TARGET_ARCH}")
 endif()
 
 if(${STRINGZILLA_BUILD_TEST})
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bd903210..da369582 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -45,7 +45,7 @@ You can download them using the following commands:
 ```sh
 # English Leipzig Corpora Collection
 # 124 MB, 1'000'000 lines of ASCII, 8'388'608 tokens of mean length 5
-wget --no-clobber -O leipzig1M.txt https://introcs.cs.princeton.edu/python/42sort/leipzig1m.txt 
+wget --no-clobber -O leipzig1M.txt https://introcs.cs.princeton.edu/python/42sort/leipzig1m.txt
 
 # Hutter Prize "enwik9" dataset for compression
 # 1 GB (0.3 GB compressed), 13'147'025 lines of ASCII, 67'108'864 tokens of mean length 6
@@ -55,7 +55,7 @@ unzip enwik9.zip && rm enwik9.zip && mv enwik9 enwik9.txt
 # XL Sum dataset for multilingual extractive summarization
 # 4.7 GB (1.7 GB compressed), 1'004'598 lines of UTF8, 268'435'456 tokens of mean length 8
 wget --no-clobber -O xlsum.csv.gz https://github.com/ashvardanian/xl-sum/releases/download/v1.0.0/xlsum.csv.gz
-gzip -d xlsum.csv.gz
+gzip -d  xlsum.csv.gz
 
 # Human chromosome generator dataset generated by:
 # 1200 rows, each 800 characters long (939K)
@@ -105,11 +105,22 @@ For Python code:
 The primary C implementation and the C++ wrapper are built with CMake.
 Assuming the extensive use of new SIMD intrinsics and recent C++ language features, using a recent compiler is recommended.
 We prefer GCC 12, which is available from default Ubuntu repositories with Ubuntu 22.04 LTS onwards.
-If this is your first experience with CMake, use the following commands to get started:
+If this is your first experience with CMake, use the following commands to get started on Ubuntu:
 
 ```bash
-sudo apt-get update && sudo apt-get install cmake build-essential libjemalloc-dev g++-12 gcc-12 # Ubuntu
-brew install libomp llvm # MacOS
+sudo apt-get update && sudo apt-get install cmake build-essential libjemalloc-dev g++-12 gcc-12
+```
+
+On MacOS it's recommended to use Homebrew and install Clang, as opposed to "Apple Clang".
+Replacing the default compiler is not recommended, as it may break the system, but you can pass it as an environment variable:
+
+```bash
+brew install llvm
+cmake -D CMAKE_BUILD_TYPE=Release -D SIMSIMD_BUILD_TESTS=1 \
+    -D CMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" \
+    -D CMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" \
+    -B build_release
+cmake --build build_release --config Release
 ```
 
 ### Testing
@@ -117,7 +128,7 @@ brew install libomp llvm # MacOS
 Using modern syntax, this is how you build and run the test suite:
 
 ```bash
-cmake -DSTRINGZILLA_BUILD_TEST=1 -DCMAKE_BUILD_TYPE=Debug -B build_debug
+cmake -D STRINGZILLA_BUILD_TEST=1 -D CMAKE_BUILD_TYPE=Debug -B build_debug
 cmake --build build_debug --config Debug          # Which will produce the following targets:
 build_debug/stringzilla_test_cpp20                # Unit test for the entire library compiled for current hardware
 build_debug/stringzilla_test_cpp20_x86_serial     # x86 variant compiled for IvyBridge - last arch. before AVX2
@@ -131,10 +142,10 @@ Overall, CppCheck and Clang-Tidy are extremely noisy and not suitable for CI, bu
 sudo apt install cppcheck clang-tidy-11
 
 cmake -B build_artifacts \
-  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-  -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
-  -DSTRINGZILLA_BUILD_BENCHMARK=1 \
-  -DSTRINGZILLA_BUILD_TEST=1
+  -D CMAKE_BUILD_TYPE=RelWithDebInfo \
+  -D CMAKE_EXPORT_COMPILE_COMMANDS=1 \
+  -D STRINGZILLA_BUILD_BENCHMARK=1 \
+  -D STRINGZILLA_BUILD_TEST=1
 
 cppcheck --project=build_artifacts/compile_commands.json --enable=all
 
@@ -145,20 +156,22 @@ I'd recommend putting the following breakpoints:
 
 - `__asan::ReportGenericError` - to detect illegal memory accesses.
 - `__GI_exit` - to stop at exit points - the end of running any executable.
-- `__builtin_unreachable` - to catch all the places where the code is expected to be unreachable.
+- `__builtin_unreachable` - to catch unexpected code paths.
+- `_sz_assert_failure` - to catch StringZilla logic assertions.
 
 ### Benchmarking
 
 For benchmarks, you can use the following commands:
 
 ```bash
-cmake -DSTRINGZILLA_BUILD_BENCHMARK=1 -B build_release
-cmake --build build_release --config Release      # Which will produce the following targets:
-build_release/stringzilla_bench_search <path>     # for substring search
-build_release/stringzilla_bench_token <path>      # for hashing, equality comparisons, etc.
-build_release/stringzilla_bench_similarity <path> # for edit distances and alignment scores
-build_release/stringzilla_bench_sort <path>       # for sorting arrays of strings
-build_release/stringzilla_bench_container <path>  # for STL containers with string keys
+cmake -D STRINGZILLA_BUILD_BENCHMARK=1 -B build_release
+cmake --build build_release --config Release      # Produces the following targets:
+build_release/stringzilla_bench_memory <path>     # - for string copies and fills
+build_release/stringzilla_bench_search <path>     # - for substring search
+build_release/stringzilla_bench_token <path>      # - for hashing, equality comparisons, etc.
+build_release/stringzilla_bench_similarity <path> # - for edit distances and alignment scores
+build_release/stringzilla_bench_sort <path>       # - for sorting arrays of strings
+build_release/stringzilla_bench_container <path>  # - for STL containers with string keys
 ```
 
 ### Benchmarking Hardware-Specific Optimizations
@@ -168,14 +181,14 @@ The assumption would be that newer ISA extensions would provide better performan
 On x86_64, you can use the following commands to compile for Sandy Bridge, Haswell, and Sapphire Rapids:
 
 ```bash
-cmake -DCMAKE_BUILD_TYPE=Release -DSTRINGZILLA_BUILD_BENCHMARK=1 \
-    -DSTRINGZILLA_TARGET_ARCH="ivybridge" -B build_release/ivybridge && \
+cmake -D CMAKE_BUILD_TYPE=Release -D STRINGZILLA_BUILD_BENCHMARK=1 \
+    -D STRINGZILLA_TARGET_ARCH="ivybridge" -B build_release/ivybridge && \
     cmake --build build_release/ivybridge --config Release
-cmake -DCMAKE_BUILD_TYPE=Release -DSTRINGZILLA_BUILD_BENCHMARK=1 \
-    -DSTRINGZILLA_TARGET_ARCH="haswell" -B build_release/haswell && \
+cmake -D CMAKE_BUILD_TYPE=Release -D STRINGZILLA_BUILD_BENCHMARK=1 \
+    -D STRINGZILLA_TARGET_ARCH="haswell" -B build_release/haswell && \
     cmake --build build_release/haswell --config Release
-cmake -DCMAKE_BUILD_TYPE=Release -DSTRINGZILLA_BUILD_BENCHMARK=1 \
-    -DSTRINGZILLA_TARGET_ARCH="sapphirerapids" -B build_release/sapphirerapids && \
+cmake -D CMAKE_BUILD_TYPE=Release -D STRINGZILLA_BUILD_BENCHMARK=1 \
+    -D STRINGZILLA_TARGET_ARCH="sapphirerapids" -B build_release/sapphirerapids && \
     cmake --build build_release/sapphirerapids --config Release
 ```
 
@@ -185,11 +198,11 @@ Alternatively, you may want to compare the performance of the code compiled with
 On x86_64, you may want to compare GCC, Clang, and ICX.
 
 ```bash
-cmake -DCMAKE_BUILD_TYPE=Release -DSTRINGZILLA_BUILD_BENCHMARK=1 -DSTRINGZILLA_BUILD_SHARED=1 \
-    -DCMAKE_CXX_COMPILER=g++-12 -DCMAKE_C_COMPILER=gcc-12 \
+cmake -D CMAKE_BUILD_TYPE=Release -D STRINGZILLA_BUILD_BENCHMARK=1 -D STRINGZILLA_BUILD_SHARED=1 \
+    -D CMAKE_CXX_COMPILER=g++-12 -D CMAKE_C_COMPILER=gcc-12 \
     -B build_release/gcc && cmake --build build_release/gcc --config Release
-cmake -DCMAKE_BUILD_TYPE=Release -DSTRINGZILLA_BUILD_BENCHMARK=1 -DSTRINGZILLA_BUILD_SHARED=1 \
-    -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_C_COMPILER=clang-14 \
+cmake -D CMAKE_BUILD_TYPE=Release -D STRINGZILLA_BUILD_BENCHMARK=1 -D STRINGZILLA_BUILD_SHARED=1 \
+    -D CMAKE_CXX_COMPILER=clang++-14 -D CMAKE_C_COMPILER=clang-14 \
     -B build_release/clang && cmake --build build_release/clang --config Release
 ```
 
@@ -199,10 +212,10 @@ To simplify tracing and profiling, build with symbols using the `RelWithDebInfo`
 Here is an example for profiling one target - `stringzilla_bench_token`.
 
 ```bash
-cmake -DSTRINGZILLA_BUILD_BENCHMARK=1 \
-    -DSTRINGZILLA_BUILD_TEST=1 \
-    -DSTRINGZILLA_BUILD_SHARED=1 \
-    -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+cmake -D STRINGZILLA_BUILD_BENCHMARK=1 \
+    -D STRINGZILLA_BUILD_TEST=1 \
+    -D STRINGZILLA_BUILD_SHARED=1 \
+    -D CMAKE_BUILD_TYPE=RelWithDebInfo \
     -B build_profile
 cmake --build build_profile --config Release --target stringzilla_bench_token
 
@@ -229,7 +242,7 @@ The base image is only ~3 MB, and it's based on musl libc, which is different fr
 sudo docker run -it --rm -v "$(pwd)":/workspace/StringZilla alpine:latest /bin/ash
 cd /workspace/StringZilla
 apk add --update make cmake g++ gcc
-cmake -DSTRINGZILLA_BUILD_TEST=1 -DCMAKE_BUILD_TYPE=Debug -B build_debug
+cmake -D STRINGZILLA_BUILD_TEST=1 -D CMAKE_BUILD_TYPE=Debug -B build_debug
 cmake --build build_debug --config Debug
 build_debug/stringzilla_test_cpp20
 ```
@@ -237,7 +250,7 @@ build_debug/stringzilla_test_cpp20
 #### Intel Clear Linux
 
 Clear Linux is a distribution optimized for Intel hardware, and is known for its performance.
-It has rolling releases, and is based on glibc.
+It has rolling releases, and is based on `glibc`.
 It might be a good choice for compiling with Intel oneAPI compilers.
 
 ```bash
@@ -245,7 +258,7 @@ sudo docker run -it --rm -v "$(pwd)":/workspace/StringZilla clearlinux:latest /b
 cd /workspace/StringZilla
 swupd update
 swupd bundle-add c-basic dev-utils
-cmake -DSTRINGZILLA_BUILD_TEST=1 -DCMAKE_BUILD_TYPE=Debug -B build_debug
+cmake -D STRINGZILLA_BUILD_TEST=1 -D CMAKE_BUILD_TYPE=Debug -B build_debug
 cmake --build build_debug --config Debug
 build_debug/stringzilla_test_cpp20
 ```
@@ -253,7 +266,7 @@ build_debug/stringzilla_test_cpp20
 For benchmarks:
 
 ```bash
-cmake -DSTRINGZILLA_BUILD_TEST=1 -DSTRINGZILLA_BUILD_BENCHMARK=1 -B build_release
+cmake -D STRINGZILLA_BUILD_TEST=1 -D STRINGZILLA_BUILD_BENCHMARK=1 -B build_release
 cmake --build build_release --config Release
 ```
 
@@ -265,8 +278,8 @@ For CentOS-based __Amazon Linux 2023__:
 sudo docker run -it --rm -v "$(pwd)":/workspace/StringZilla amazonlinux:2023 bash
 cd /workspace/StringZilla
 yum install -y make cmake3 gcc g++
-cmake3 -DSTRINGZILLA_BUILD_TEST=1 -DCMAKE_BUILD_TYPE=Debug \
-    -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc -DSTRINGZILLA_TARGET_ARCH="ivybridge" \
+cmake3 -D STRINGZILLA_BUILD_TEST=1 -D CMAKE_BUILD_TYPE=Debug \
+    -D CMAKE_CXX_COMPILER=g++ -D CMAKE_C_COMPILER=gcc -D STRINGZILLA_TARGET_ARCH="ivybridge" \
     -B build_debug
 cmake3 --build build_debug --config Debug --target stringzilla_test_cpp11
 build_debug/stringzilla_test_cpp11
@@ -279,8 +292,8 @@ Sadly, the newest GCC version it supports is 10, and it can't handle AVX-512 ins
 sudo docker run -it --rm -v "$(pwd)":/workspace/StringZilla amazonlinux:2 bash
 cd /workspace/StringZilla
 yum install -y make cmake3 gcc10 gcc10-c++
-cmake3 -DSTRINGZILLA_BUILD_TEST=1 -DCMAKE_BUILD_TYPE=Debug \
-    -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc -DSTRINGZILLA_TARGET_ARCH="ivybridge" \
+cmake3 -D STRINGZILLA_BUILD_TEST=1 -D CMAKE_BUILD_TYPE=Debug \
+    -D CMAKE_CXX_COMPILER=g++ -D CMAKE_C_COMPILER=gcc -D STRINGZILLA_TARGET_ARCH="ivybridge" \
     -B build_debug
 cmake3 --build build_debug --config Debug --target stringzilla_test_cpp11
 build_debug/stringzilla_test_cpp11
@@ -328,11 +341,11 @@ export RANLIB="llvm-ranlib"
 export TARGET_ARCH="aarch64-linux-gnu" # Or "x86_64-linux-gnu"
 export BUILD_ARCH="arm64" # Or "amd64"
 
-cmake -DCMAKE_BUILD_TYPE=Release \
-    -DCMAKE_C_COMPILER_TARGET=${TARGET_ARCH} \
-    -DCMAKE_CXX_COMPILER_TARGET=${TARGET_ARCH} \
-    -DCMAKE_SYSTEM_NAME=Linux \
-    -DCMAKE_SYSTEM_PROCESSOR=${BUILD_ARCH} \
+cmake -D CMAKE_BUILD_TYPE=Release \
+    -D CMAKE_C_COMPILER_TARGET=${TARGET_ARCH} \
+    -D CMAKE_CXX_COMPILER_TARGET=${TARGET_ARCH} \
+    -D CMAKE_SYSTEM_NAME=Linux \
+    -D CMAKE_SYSTEM_PROCESSOR=${BUILD_ARCH} \
     -B build_artifacts
 cmake --build build_artifacts --config Release
 ```
@@ -376,7 +389,7 @@ cibuildwheel --platform macos                   # works only on MacOS
 cibuildwheel --platform windows                 # works only on Windows
 ```
 
-You may need root previligies for multi-architecture builds:
+You may need root privileges for multi-architecture builds:
 
 ```bash
 sudo $(which cibuildwheel) --platform linux
@@ -398,7 +411,7 @@ For benchmarking, the following scripts are provided.
 ```sh
 python scripts/bench_search.py --haystack_path "your file" --needle "your pattern" # real data
 python scripts/bench_search.py --haystack_pattern "abcd" --haystack_length 1e9 --needle "abce" # synthetic data
-python scripts/similarity_bench.py --text_path "your file" # edit ditance computations
+python scripts/similarity_bench.py --text_path "your file" # edit distance computations
 ```
 
 Alternatively, you can explore the Jupyter notebooks in `scripts/` directory.
@@ -458,13 +471,22 @@ cargo package --list --allow-dirty
 
 If you want to run benchmarks against third-party implementations, check out the [`ashvardanian/memchr_vs_stringzilla`](https://github.com/ashvardanian/memchr_vs_stringzilla/) repository.
 
-## General Performance Observations
+## General Recommendations
+
+### Operations Not Worth Optimizing
+
+One of the hardest things to learn in HPC is when to stop optimizing, and where not to start.
+
+It doesn't make sense to optimize `sz_order`, because almost always, the relative order of two strings depends on the first bytes.
+Fetching more bytes is not worth it.
+In `sz_equal`, however, in rare cases, SIMD can help, if the user is comparing two mostly similar strings with identical hashes or checksums.
 
 ### Unaligned Loads
 
 One common surface of attack for performance optimizations is minimizing unaligned loads.
 Such solutions are beautiful from the algorithmic perspective, but often lead to worse performance.
 It's often cheaper to issue two interleaving wide-register loads, than try minimizing those loads at the cost of juggling registers.
+Unaligned stores are a different story, especially on x86, where multiple reads can be issued in parallel, but only one write can be issued at a time.
 
 ### Register Pressure
 
@@ -490,12 +512,20 @@ if (matches0 | matches1 | matches2 | matches3)
 A simpler solution would be to compare byte-by-byte, but in that case we would need to populate multiple registers, broadcasting different letters of the needle into them.
 That may not be noticeable on a micro-benchmark, but it would be noticeable on real-world workloads, where the CPU will speculatively interleave those search operations with something else happening in that context.
 
-## Working on Alternative Hardware Backends
+### Working on Alternative Hardware Backends
 
-## Working on Faster Edit Distances
+It's important to keep compiler support in mind when extending to new instruction sets.
+Check the most recent CI pipeline configurations in `prerelease.yml` and `release.yml` to see which compilers are used.
+When implementing dynamic dispatch, avoid compiler intrinsics and OS-specific APIs, as they may not be available on all platforms.
+Instead, use inline assembly to check feature flags and dispatch them to the proper implementation.
 
-## Working on Random String Generators
+### Working on Faster Edit Distances
 
-## Working on Sequence Processing and Sorting
+When dealing with non-trivial algorithms, like edit distances, it's advisory to provide pseudo-code or a reference implementation in addition to the optimized one.
+Ideally, include it in `scripts/` as a Python Jupyter Notebook with explanations and visualizations.
 
+### Working on Sequence Processing and Sorting
 
+Sorting algorithms for strings are a deeply studied area.
+In general, string sorting algorithms discourage the use of comparisons, as they are expensive for variable-length data and also require pointer-chasing for most array layouts.
+They are also harder to accelerate with SIMD, as most layouts imply 16-byte entries, which are often too big to benefit from simple SIMD techniques.
\ No newline at end of file
diff --git a/README.md b/README.md
index 09b3ef96..40b3258f 100644
--- a/README.md
+++ b/README.md
@@ -766,6 +766,25 @@ To safely print those, pass the `string_length` to `printf` as well.
 printf("%.*s\n", (int)string_length, string_start);
 ```
 
+### What's Wrong with the C Standard Library?
+
+StringZilla is not a drop-in replacement for the C Standard Library.
+It's designed to be a safer and more modern alternative.
+Conceptually:
+
+1. LibC strings are expected to be null-terminated, so to use the efficient LibC implementations on slices of larger strings, you'd have to copy them, which is more expensive than the original string operation.
+2. LibC functionality is asymmetric - you can find the first and the last occurrence of a character within a string, but you can't find the last occurrence of a substring.
+3. LibC function names are typically very short and cryptic.
+4. LibC lacks crucial functionality like hashing and doesn't provide primitives for less critical but relevant operations like fuzzy matching.
+
+Something has to be said about its support for UTF8.
+Aside from a single-byte `char` type, LibC provides `wchar_t`:
+
+- The size of `wchar_t` is not consistent across platforms. On Windows, it's typically 16 bits (suitable for UTF-16), while on Unix-like systems, it's usually 32 bits (suitable for UTF-32). This inconsistency can lead to portability issues when writing cross-platform code.
+- `wchar_t` is designed to represent wide characters in a fixed-width format (UTF-16 or UTF-32). In contrast, UTF-8 is a variable-length encoding, where each character can take from 1 to 4 bytes. This fundamental difference means that `wchar_t` and UTF-8 are incompatible.
+
+StringZilla [partially addresses those issues](#unicode-utf-8-and-wide-characters).
+
 ### What's Wrong with the C++ Standard Library?
 
 | C++ Code                             | Evaluation Result | Invoked Signature              |
@@ -849,9 +868,9 @@ StringZilla provides a convenient `partition` function, which returns a tuple of
 ```cpp
 auto parts = haystack.partition(':'); // Matching a character
 auto [before, match, after] = haystack.partition(':'); // Structure unpacking
-auto [before, match, after] = haystack.partition(char_set(":;")); // Character-set argument
+auto [before, match, after] = haystack.partition(sz::char_set(":;")); // Character-set argument
 auto [before, match, after] = haystack.partition(" : "); // String argument
-auto [before, match, after] = haystack.rpartition(sz::whitespaces); // Split around the last whitespace
+auto [before, match, after] = haystack.rpartition(sz::whitespaces_set()); // Split around the last whitespace
 ```
 
 Combining those with the `split` function, one can easily parse a CSV file or HTTP headers.
@@ -877,8 +896,8 @@ Here is a sneak peek of the most useful ones.
 ```cpp
 text.hash(); // -> 64 bit unsigned integer 
 text.ssize(); // -> 64 bit signed length to avoid `static_cast<std::ssize_t>(text.size())`
-text.contains_only(" \w\t"); // == text.find_first_not_of(char_set(" \w\t")) == npos;
-text.contains(sz::whitespaces); // == text.find(char_set(sz::whitespaces)) != npos;
+text.contains_only(" \w\t"); // == text.find_first_not_of(sz::char_set(" \w\t")) == npos;
+text.contains(sz::whitespaces_set()); // == text.find(sz::char_set(sz::whitespaces_set())) != npos;
 
 // Simpler slicing than `substr`
 text.front(10); // -> sz::string_view
@@ -890,9 +909,9 @@ text.front(10, cap) == text.front(std::min(10, text.size()));
 text.back(10, cap) == text.back(std::min(10, text.size()));
 
 // Character set filtering
-text.lstrip(sz::whitespaces).rstrip(sz::newlines); // like Python
-text.front(sz::whitespaces); // all leading whitespaces
-text.back(sz::digits); // all numerical symbols forming the suffix
+text.lstrip(sz::whitespaces_set()).rstrip(sz::newlines_set()); // like Python
+text.front(sz::whitespaces_set()); // all leading whitespaces
+text.back(sz::digits_set()); // all numerical symbols forming the suffix
 
 // Incremental construction
 using sz::string::unchecked;
@@ -923,7 +942,7 @@ To avoid those, StringZilla provides lazily-evaluated ranges, compatible with th
 
 ```cpp
 for (auto line : haystack.split("\r\n"))
-    for (auto word : line.split(char_set(" \w\t.,;:!?")))
+    for (auto word : line.split(sz::char_set(" \w\t.,;:!?")))
         std::cout << word << std::endl;
 ```
 
@@ -932,9 +951,9 @@ It also allows interleaving matches, if you want both inclusions of `xx` in `xxx
 Debugging pointer offsets is not a pleasant exercise, so keep the following functions in mind.
 
 - `haystack.[r]find_all(needle, interleaving)`
-- `haystack.[r]find_all(char_set(""))`
+- `haystack.[r]find_all(sz::char_set(""))`
 - `haystack.[r]split(needle)`
-- `haystack.[r]split(char_set(""))`
+- `haystack.[r]split(sz::char_set(""))`
 
 For $N$ matches the split functions will report $N+1$ matches, potentially including empty strings.
 Ranges have a few convenience methods as well:
@@ -1348,6 +1367,23 @@ With that solved, the SIMD implementation will become 5x faster than the serial
 [faq-dipeptide]: https://en.wikipedia.org/wiki/Dipeptide
 [faq-titin]: https://en.wikipedia.org/wiki/Titin
 
+### Memory Copying, Fills, and Moves
+
+A lot has been written about the time computers spend copying memory and how that operation is implemented in LibC.
+Interestingly, the operation can still be improved, as most Assembly implementations use outdated instructions.
+Even performance-oriented STL replacements, like Meta's [Folly v2024.09.23 focus on AVX2](https://github.com/facebook/folly/blob/main/folly/memset.S), and don't take advantage of the new masked instructions in AVX-512 or SVE.
+
+In AVX-512, StringZilla uses non-temporal stores to avoid cache pollution, when dealing with very large strings.
+Moreover, it handles the unaligned head and the tails of the `target` buffer separately, ensuring that writes in big copies are always aligned to cache-line boundaries.
+That's true for both AVX2 and AVX-512 backends.
+
+StringZilla also contains "drafts" of smarter, but less efficient algorithms, that minimize the number of unaligned loads, perfoming shuffles and permutations.
+That's a topic for future research, as the performance gains are not yet satisfactory.
+
+> § Reading materials.
+> [`memset` benchmarks](https://github.com/nadavrot/memset_benchmark?tab=readme-ov-file) by Nadav Rotem.
+> [Cache Associativity](https://en.algorithmica.org/hpc/cpu-cache/associativity/) by Sergey Slotin.
+
 ### Random Generation
 
 Generating random strings from different alphabets is a very common operation.
@@ -1368,11 +1404,8 @@ For lexicographic sorting of strings, StringZilla uses a "hybrid-hybrid" approac
    1. IntroSort begins with a QuickSort.
    2. If the recursion depth exceeds a certain threshold, it switches to a HeapSort.
 
-Next design goals:
-
-- [ ] Generalize to arrays with over 4 billion entries.
-- [ ] Algorithmic improvements may yield another 3x performance gain.
-- [ ] SIMD-acceleration for the Radix slice.
+A better algorithm is in development.
+Check #173 for design goals and progress updates.
 
 ### Hashing
 
diff --git a/c/lib.c b/c/lib.c
index edffd14e..e523e377 100644
--- a/c/lib.c
+++ b/c/lib.c
@@ -119,6 +119,7 @@ typedef struct sz_implementations_t {
     sz_move_t copy;
     sz_move_t move;
     sz_fill_t fill;
+    sz_look_up_transform_t look_up_transform;
 
     sz_find_byte_t find_byte;
     sz_find_byte_t rfind_byte;
@@ -153,6 +154,7 @@ static void sz_dispatch_table_init(void) {
     impl->copy = sz_copy_serial;
     impl->move = sz_move_serial;
     impl->fill = sz_fill_serial;
+    impl->look_up_transform = sz_look_up_transform_serial;
 
     impl->find = sz_find_serial;
     impl->rfind = sz_rfind_serial;
@@ -167,9 +169,14 @@ static void sz_dispatch_table_init(void) {
 
 #if SZ_USE_X86_AVX2
     if (caps & sz_cap_x86_avx2_k) {
+        impl->equal = sz_equal_avx2;
+        impl->order = sz_order_avx2;
+
         impl->copy = sz_copy_avx2;
         impl->move = sz_move_avx2;
         impl->fill = sz_fill_avx2;
+        impl->look_up_transform = sz_look_up_transform_avx2;
+
         impl->find_byte = sz_find_byte_avx2;
         impl->rfind_byte = sz_rfind_byte_avx2;
         impl->find = sz_find_avx2;
@@ -183,6 +190,7 @@ static void sz_dispatch_table_init(void) {
     if (caps & sz_cap_x86_avx512f_k) {
         impl->equal = sz_equal_avx512;
         impl->order = sz_order_avx512;
+
         impl->copy = sz_copy_avx512;
         impl->move = sz_move_avx512;
         impl->fill = sz_fill_avx512;
@@ -200,11 +208,19 @@ static void sz_dispatch_table_init(void) {
         impl->find_from_set = sz_find_charset_avx512;
         impl->rfind_from_set = sz_rfind_charset_avx512;
         impl->alignment_score = sz_alignment_score_avx512;
+        impl->look_up_transform = sz_look_up_transform_avx512;
     }
 #endif
 
 #if SZ_USE_ARM_NEON
     if (caps & sz_cap_arm_neon_k) {
+        impl->equal = sz_equal_neon;
+
+        impl->copy = sz_copy_neon;
+        impl->move = sz_move_neon;
+        impl->fill = sz_fill_neon;
+        impl->look_up_transform = sz_look_up_transform_neon;
+
         impl->find = sz_find_neon;
         impl->rfind = sz_rfind_neon;
         impl->find_byte = sz_find_byte_neon;
@@ -256,6 +272,10 @@ SZ_DYNAMIC void sz_fill(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
     sz_dispatch_table.fill(target, length, value);
 }
 
+SZ_DYNAMIC void sz_look_up_transform(sz_cptr_t source, sz_size_t length, sz_cptr_t lut, sz_ptr_t target) {
+    sz_dispatch_table.look_up_transform(source, length, lut, target);
+}
+
 SZ_DYNAMIC sz_cptr_t sz_find_byte(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle) {
     return sz_dispatch_table.find_byte(haystack, h_length, needle);
 }
diff --git a/include/stringzilla/drafts.h b/include/stringzilla/drafts.h
index 17147f7e..bcba2233 100644
--- a/include/stringzilla/drafts.h
+++ b/include/stringzilla/drafts.h
@@ -952,4 +952,374 @@ SZ_PUBLIC void sz_hashes_neon_readahead(sz_cptr_t start, sz_size_t length, sz_si
 } // extern "C"
 #endif
 
-#endif // STRINGZILLA_EXPERIMENTAL_H_
\ No newline at end of file
+#endif // STRINGZILLA_EXPERIMENTAL_H_
+
+SZ_PUBLIC sz_ordering_t sz_order_avx2(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length) {
+
+    // _bswap64;
+
+    // while (a_length >= 8 && b_length >= 8) {
+    //     sz_u64_t a_u64 = *(sz_u64_t *)a;
+    //     sz_u64_t b_u64 = *(sz_u64_t *)b;
+    //     if (a_u64 != b_u64) return _sz_order_scalars(a_u64, b_u64);
+    //     a += 8, b += 8, a_length -= 8, b_length -= 8;
+    // }
+
+    // The rare case, when both string are very long surves as a great example to understand
+    // the basic logic of the algorithm without the complexity of `("abc\0" < "abc")` corner cases.
+    while ((a_length >= 64) & (b_length >= 64)) {
+        a_vec.zmm = _mm512_loadu_si512(a);
+        b_vec.zmm = _mm512_loadu_si512(b);
+        // The AVX-512 `_mm512_mask_cmpneq_epi8_mask` intrinsics are generally handy in such environments.
+        // They, however, have latency 3 on most modern CPUs. Using AVX2: `_mm256_cmpeq_epi8` would have
+        // been cheaper, if we didn't have to apply `_mm256_movemask_epi8` afterwards.
+        //
+        //      __mmask64 mask_not_equal = _mm512_cmpneq_epi8_mask(a_vec.zmm, b_vec.zmm);
+        //      if (mask_not_equal != 0) {
+        //          sz_u64_t first_diff = _tzcnt_u64(mask_not_equal);
+        //          char a_char = a[first_diff];
+        //          char b_char = b[first_diff];
+        //          return _sz_order_scalars(a_char, b_char);
+        //      }
+        //
+        // A wiser approach to avoid serial code, is to perform 2 vector comparisons instead of quality check.
+        __mmask64 less_mask = _mm512_cmplt_epu8_mask(a_vec.zmm, b_vec.zmm);
+        __mmask64 greater_mask = _mm512_cmpgt_epu8_mask(a_vec.zmm, b_vec.zmm);
+        // Let's assume both strings are exactly 64 bytes long, like `("abcdabcd..." < "acbdacbd...")`.
+        // In that case:
+        //      - if `less_mask == 0 && greater_mask == 0`, the strings are equal, and we can skip 64 bytes.
+        //      - if `_tzcnt_u64(less_mask) < _tzcnt_u64(greater_mask)` than the first string is less than the second.
+        // The `_tzcnt_u64` trailing zeros computation, however, also has latency of 3 cycles.
+        unsigned char all_equal = _kortestz_mask8_u8(less_mask, greater_mask);
+        if (all_equal) { a += 64, b += 64, a_length -= 64, b_length -= 64; }
+        else { return _sz_order_scalars(_tzcnt_u64(less_mask), _tzcnt_u64(greater_mask)); }
+    }
+
+    // Assume a case like `("abc\0" < "abc")`.
+    // Knowing the length masks of both strings, we can find the bytes that make up the difference
+    // and enable them in the `greater_mask`, to signal the presence of null-characters in the end.
+    //
+    //      __mmask64 a_mask = _sz_u64_clamp_mask_until(a_length);
+    //      __mmask64 b_mask = _sz_u64_clamp_mask_until(b_length);
+    //      a_vec.zmm = _mm512_maskz_loadu_epi8(a_mask, a);
+    //      b_vec.zmm = _mm512_maskz_loadu_epi8(b_mask, b);
+    //      __mmask64 after_a_before_b_mask = _kandn_mask64(a_mask, b_mask);
+    //      __mmask64 after_b_before_a_mask = _kandn_mask64(b_mask, a_mask);
+    //      __mmask64 less_mask = _mm512_cmplt_epu8_mask(a_vec.zmm, b_vec.zmm);
+    //      __mmask64 greater_mask = _mm512_cmpgt_epu8_mask(a_vec.zmm, b_vec.zmm);
+    //      less_mask = _kor_mask64(less_mask, after_a_before_b_mask);
+    //      greater_mask = _kor_mask64(greater_mask, after_b_before_a_mask);
+    //      unsigned char all_equal = _kortestz_mask8_u8(less_mask, greater_mask);
+    //      if (all_equal) { return sz_equal_k; }
+    //      else { return earlier_in_less_mask ? sz_less_k : sz_greater_k; }
+    return sz_order_serial(a, a_length, b, b_length);
+}
+
+SZ_PUBLIC sz_ordering_t sz_order_avx512(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length) {
+    sz_u512_vec_t a_vec, b_vec;
+
+    // The rare case, when both string are very long surves as a great example to understand
+    // the basic logic of the algorithm without the complexity of `("abc\0" < "abc")` corner cases.
+    while ((a_length >= 64) & (b_length >= 64)) {
+        a_vec.zmm = _mm512_loadu_si512(a);
+        b_vec.zmm = _mm512_loadu_si512(b);
+        // The AVX-512 `_mm512_mask_cmpneq_epi8_mask` intrinsics are generally handy in such environments.
+        // They, however, have latency 3 on most modern CPUs. Using AVX2: `_mm256_cmpeq_epi8` would have
+        // been cheaper, if we didn't have to apply `_mm256_movemask_epi8` afterwards.
+        //
+        //      __mmask64 mask_not_equal = _mm512_cmpneq_epi8_mask(a_vec.zmm, b_vec.zmm);
+        //      if (mask_not_equal != 0) {
+        //          sz_u64_t first_diff = _tzcnt_u64(mask_not_equal);
+        //          char a_char = a[first_diff];
+        //          char b_char = b[first_diff];
+        //          return _sz_order_scalars(a_char, b_char);
+        //      }
+        //
+        // A wiser approach to avoid serial code, is to perform 2 vector comparisons instead of quality check.
+        __mmask64 less_mask = _mm512_cmplt_epu8_mask(a_vec.zmm, b_vec.zmm);
+        __mmask64 greater_mask = _mm512_cmpgt_epu8_mask(a_vec.zmm, b_vec.zmm);
+        // Let's assume both strings are exactly 64 bytes long, like `("abcdabcd..." < "acbdacbd...")`.
+        // In that case:
+        //      - if `less_mask == 0 && greater_mask == 0`, the strings are equal, and we can skip 64 bytes.
+        //      - if `_tzcnt_u64(less_mask) < _tzcnt_u64(greater_mask)` than the first string is less than the second.
+        // The `_tzcnt_u64` trailing zeros computation, however, also has latency of 3 cycles.
+        unsigned char all_equal = _kortestz_mask8_u8(less_mask, greater_mask);
+        if (all_equal) { a += 64, b += 64, a_length -= 64, b_length -= 64; }
+        else { return _sz_order_scalars(_tzcnt_u64(less_mask), _tzcnt_u64(greater_mask)); }
+    }
+
+    // Assume a case like `("abc\0" < "abc")`.
+    // Knowing the length masks of both strings, we can find the bytes that make up the difference
+    // and enable them in the `greater_mask`, to signal the presence of null-characters in the end.
+    //
+    //      __mmask64 a_mask = _sz_u64_clamp_mask_until(a_length);
+    //      __mmask64 b_mask = _sz_u64_clamp_mask_until(b_length);
+    //      a_vec.zmm = _mm512_maskz_loadu_epi8(a_mask, a);
+    //      b_vec.zmm = _mm512_maskz_loadu_epi8(b_mask, b);
+    //      __mmask64 after_a_before_b_mask = _kandn_mask64(a_mask, b_mask);
+    //      __mmask64 after_b_before_a_mask = _kandn_mask64(b_mask, a_mask);
+    //      __mmask64 less_mask = _mm512_cmplt_epu8_mask(a_vec.zmm, b_vec.zmm);
+    //      __mmask64 greater_mask = _mm512_cmpgt_epu8_mask(a_vec.zmm, b_vec.zmm);
+    //      less_mask = _kor_mask64(less_mask, after_a_before_b_mask);
+    //      greater_mask = _kor_mask64(greater_mask, after_b_before_a_mask);
+    //      unsigned char all_equal = _kortestz_mask8_u8(less_mask, greater_mask);
+    //      if (all_equal) { return sz_equal_k; }
+    //      else { return earlier_in_less_mask ? sz_less_k : sz_greater_k; }
+    return sz_order_serial(a, a_length, b, b_length);
+}
+
+SZ_PUBLIC void sz_move_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
+    if (target == source) return; // Don't be silly, don't move the data if it's already there.
+
+    // If the regions don't overlap at all, just use "copy" and save some brain cells thinking about corner cases.
+    if (target + length < source || target >= source + length) {
+        sz_copy_avx512(target, source, length);
+        return;
+    }
+
+    // The absolute most common case of using "moves" is shifting the data within a continuous buffer
+    // when adding a removing some values in it. In such cases, a typical shift is by 1, 2, 4, 8, 16,
+    // or 32 bytes, rarely larger. For small shifts, under the size of the ZMM register, we can use shuffles.
+    //
+    // Remember: if we are shifting data left, that we are traversing to the right.
+    int left_to_right_traversal = source > target;
+    sz_size_t shift = left_to_right_traversal ? source - target : target - source;
+
+    if (left_to_right_traversal) {
+
+        // Shift until we reach the ZMM register boundary for the target to avoid unaligned loads.
+        for (; (sz_size_t)target % 64 != 0 && length; ++target, ++source, --length) *target = *source;
+
+        // Small shifts of large buffers can minimize the number of times a specific cache line will be touched
+        // to guarantee one read and one write per cache line.
+        if (shift < 64 && length >= 128) {
+
+            // Now we guarantee, that the shift is from 1 to 63 bytes and the output is aligned.
+            // Hopefully, we need to shift more than two ZMM registers, so we could consider `valignr` instruction.
+            // Sadly, using `_mm512_alignr_epi8` doesn't make sense, as it operates at a 128-bit granularity.
+            //
+            //      - `_mm256_alignr_epi8` shifts entire 256-bit register, but we need many of them.
+            //      - `_mm512_alignr_epi32` shifts 512-bit chunks, but only if the `shift` is a multiple of 4 bytes.
+            //      - `_mm512_alignr_epi64` shifts 512-bit chunks by 8 bytes.
+            //
+            // All of those have a latency of 1 cycle, and the shift amount must be an immediate value!
+            // For 1-byte-shift granularity, the `_mm512_permutex2var_epi8` has a latency of 6 and needs VBMI!
+            // The most efficient and broadly compatible alternative would be to use a combination of align and shuffle.
+            // A similar approach was outlined in "Byte-wise alignr in AVX512F" by Wojciech Muła.
+            // http://0x80.pl/notesen/2016-10-16-avx512-byte-alignr.html
+            //
+            // That solution, is extremely mouthful, assuming we need compile time constants for the shift amount.
+            sz_u512_vec_t first_vec, second_vec, combined_vec;
+            // The last `64 - shift` entries of the first register should be moved to its start.
+            // The first `shift` entries of the second register should be moved to its end.
+            // Then we will combine:
+            //      - the first `64 - shift` entries of the first register with
+            //      - the first `shift` entries of the second register.
+#if 1
+            sz_u512_vec_t selector_vec;
+            sz_size_t shifted_idx = 0;
+            for (; shifted_idx != 64; ++shifted_idx) selector_vec.u8s[shifted_idx] = (sz_u8_t)(shift + shifted_idx);
+            // Now that the permutations are prepared, pre-load the first cache line and start the loop.
+            first_vec.zmm = _mm512_load_si512(target);
+            for (; length >= 128; target += 64, source += 64, length -= 64) {
+                second_vec.zmm = _mm512_load_si512(target + 64);
+                combined_vec.zmm = _mm512_permutex2var_epi8(first_vec.zmm, selector_vec.zmm, second_vec.zmm);
+                sz_assert(combined_vec.u8s[0] == source[0]);
+                sz_assert(combined_vec.u8s[63] == source[63]);
+                _mm512_store_si512(target, combined_vec.zmm);
+                first_vec.zmm = second_vec.zmm;
+            }
+#else
+            sz_u512_vec_t first_byte_permute_vec, second_byte_permute_vec;
+            sz_u512_vec_t first_shuffled_vec, second_shuffled_vec;
+            for (sz_size_t shifted_idx = 0; shifted_idx != (64 - shift); ++shifted_idx)
+                first_byte_permute_vec.u8s[shifted_idx] = (sz_u8_t)(shift + shifted_idx), //
+                    second_byte_permute_vec.u8s[shifted_idx] = (sz_u8_t)0xFF;
+            for (sz_size_t shifted_idx = 0; shifted_idx != shift; ++shifted_idx)
+                first_byte_permute_vec.u8s[64 - shift + shifted_idx] = (sz_u8_t)0xFF, //
+                    second_byte_permute_vec.u8s[64 - shift + shifted_idx] = (sz_u8_t)shifted_idx;
+            // The `_mm512_shuffle_epi8` only works within lanes, so we need to permute the lanes.
+            int first_lane_permute_mask, second_lane_permute_mask;
+
+            // Now that the permutations are prepared, pre-load the first cache line and start the loop.
+            first_vec.zmm = _mm512_load_si512(target);
+            for (; length >= 128; target += 64, source += 64, length -= 64) {
+                second_vec.zmm = _mm512_load_si512(target + 64);
+                first_shuffled_vec.zmm = _mm512_shuffle_epi8(first_vec.zmm, first_byte_permute_vec.zmm);
+                second_shuffled_vec.zmm = _mm512_shuffle_epi8(second_vec.zmm, second_byte_permute_vec.zmm);
+                sz_assert(first_shuffled_vec.u8s[0] == source[0]);
+                sz_assert(second_shuffled_vec.u8s[63] == source[63]);
+                combined_vec.zmm = _mm512_or_si512(first_shuffled_vec.zmm, second_shuffled_vec.zmm);
+                _mm512_store_si512(target, combined_vec.zmm);
+                first_vec.zmm = second_vec.zmm;
+            }
+#endif
+            for (; length; ++target, ++source, --length) *target = *source;
+        }
+        // With really large shifts we are not going to touch the same register on the load and store.
+        // Especially, if we align the stores to the ZMM register size.
+        else {
+            for (; length >= 64; target += 64, source += 64, length -= 64)
+                _mm512_store_si512(target, _mm512_loadu_si512(source));
+            // At this point the length is guaranteed to be under 64.
+            __mmask64 mask = _sz_u64_mask_until(length);
+            _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
+        }
+    }
+    else {
+        // Shift until we reach the ZMM register boundary for the target to avoid unaligned loads.
+        for (; (sz_size_t)(target + length) % 64 != 0 && length; --length) target[length - 1] = source[length - 1];
+        // Jump to the end and walk backwards.
+        for (target += length, source += length; length >= 64; length -= 64)
+            _mm512_store_si512(target -= 64, _mm512_loadu_si512(source -= 64));
+        // At this point the length is guaranteed to be under 64.
+        __mmask64 mask = _sz_u64_mask_until(length);
+        _mm512_mask_storeu_epi8(target - length, mask, _mm512_maskz_loadu_epi8(mask, source - length));
+    }
+}
+
+SZ_PUBLIC void sz_move_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
+    if (target == source) return; // Don't be silly, don't move the data if it's already there.
+
+    // If the regions don't overlap at all, just use "copy" and save some brain cells thinking about corner cases.
+    if (target + length < source || target >= source + length) {
+        sz_copy_avx512(target, source, length);
+        return;
+    }
+
+    // On very short buffers, that are one cache line in width or less, we don't need any loops.
+    if (length <= 64) {
+        __mmask64 mask = _sz_u64_mask_until(length);
+        _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
+        return;
+    }
+
+    // When the buffer is over 64 bytes, it's guaranteed to touch at least two cache lines - the head and tail,
+    // and may include more cache-lines in-between. Knowing this, we can avoid expensive unaligned stores
+    // by computing 2 masks - for the head and tail, using masked stores for the head and tail, and unmasked
+    // for the body.
+    sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64; // 63 or less.
+    sz_size_t tail_length = (sz_size_t)(target + length) % 64;    // 63 or less.
+    sz_size_t body_length = length - head_length - tail_length;   // Multiple of 64.
+    __mmask64 head_mask = _sz_u64_mask_until(head_length);
+    __mmask64 tail_mask = _sz_u64_mask_until(tail_length);
+
+    // The absolute most common case of using "moves" is shifting the data within a continuous buffer
+    // when adding a removing some values in it. In such cases, a typical shift is by 1, 2, 4, 8, 16,
+    // or 32 bytes, rarely larger. For small shifts, under the size of the ZMM register, we can use shuffles.
+    //
+    // Remember:
+    //      - if we are shifting data left, that we are traversing to the right.
+    //      - if we are shifting data right, that we are traversing to the left.
+    int const left_to_right_traversal = source > target;
+
+    // If both targets are equally aligned or misaligned, the efficient implementation is trivial.
+    if ((sz_size_t)target % 64 == (sz_size_t)source % 64) {
+        if (left_to_right_traversal) {
+            // Head, body, and tail.
+            _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+            target += head_length, source += head_length, body_length -= head_length;
+            for (; body_length >= 64; target += 64, source += 64, body_length -= 64)
+                _mm512_store_si512(target, _mm512_load_si512(source));
+            _mm512_mask_storeu_epi8(target, tail_mask, _mm512_maskz_loadu_epi8(tail_mask, source));
+        }
+        else {
+            // Tail, body, and head.
+            _mm512_mask_storeu_epi8(target + head_length + body_length, tail_mask,
+                                    _mm512_maskz_loadu_epi8(tail_mask, source + head_length + body_length));
+            for (; body_length >= 64; body_length -= 64)
+                _mm512_store_si512(target + head_length + body_length - 64,
+                                   _mm512_load_si512(source + head_length + body_length - 64));
+            _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+        }
+        return;
+    }
+
+    // Now we guarantee, that the relative shift within is from 1 to 63 bytes and the output is aligned.
+    // Hopefully, we need to shift more than two ZMM registers, so we could consider `valignr` instruction.
+    // Sadly, using `_mm512_alignr_epi8` doesn't make sense, as it operates at a 128-bit granularity.
+    //
+    //      - `_mm256_alignr_epi8` shifts entire 256-bit register, but we need many of them.
+    //      - `_mm512_alignr_epi32` shifts 512-bit chunks, but only if the `shift` is a multiple of 4 bytes.
+    //      - `_mm512_alignr_epi64` shifts 512-bit chunks by 8 bytes.
+    //
+    // All of those have a latency of 1 cycle, and the shift amount must be an immediate value!
+    // For 1-byte-shift granularity, the `_mm512_permutex2var_epi8` has a latency of 6 and needs VBMI!
+    // The most efficient and broadly compatible alternative could be to use a combination of align and shuffle.
+    // A similar approach was outlined in "Byte-wise alignr in AVX512F" by Wojciech Muła.
+    // http://0x80.pl/notesen/2016-10-16-avx512-byte-alignr.html
+    //
+    // That solution, is extremely mouthful, assuming we need compile time constants for the shift amount.
+    // A cleaner one, with a latency of 3 cycles, is to use `_mm512_permutexvar_epi8` or `_mm512_mask_permutexvar_epi8`,
+    // which can be seen as combination of a cross-register shuffle and blend, and is available with VBMI.
+    sz_size_t const shift = left_to_right_traversal ? source - target : target - source;
+    sz_size_t const shift_in_page = shift % 64;
+
+    if (left_to_right_traversal) {
+        // Head, body, and tail.
+        _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+        target += head_length, source += head_length;
+
+        // Define the permutation vectors for the `permute2var` instruction for the body.
+        sz_u512_vec_t first_vec, second_vec, combined_vec, selector_vec;
+        selector_vec.zmm = _mm512_set_epi8(                                 //
+            63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, //
+            47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, //
+            31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, //
+            15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
+        selector_vec.zmm = _mm512_add_epi8(selector_vec.zmm, _mm512_set1_epi8(shift_in_page));
+        selector_vec.zmm = _mm512_and_si512(selector_vec.zmm, _mm512_set1_epi8(63));
+
+        if (body_length >= 128) {
+            // Now that the permutations are prepared, pre-load the first cache line and start the loop.
+            __mmask64 blend_mask = _sz_u64_mask_until(shift_in_page);
+            sz_cptr_t source_page = source - (sz_size_t)source % 64;
+            first_vec.zmm = _mm512_load_si512(source_page);
+            for (; body_length >= 128; target += 64, source += 64, source_page += 64, body_length -= 64) {
+                second_vec.zmm = _mm512_load_si512(source_page + 64);
+                second_vec.zmm = _mm512_permutexvar_epi8(selector_vec.zmm, second_vec.zmm);
+                combined_vec.zmm = _mm512_mask_blend_epi8(blend_mask, second_vec.zmm, first_vec.zmm);
+                sz_assert(combined_vec.u8s[0] == source[0]);
+                sz_assert(combined_vec.u8s[63] == source[63]);
+                _mm512_store_si512(target, combined_vec.zmm);
+                first_vec.zmm = second_vec.zmm;
+            }
+        }
+        if (body_length)
+            _mm512_store_si512(target, _mm512_loadu_si512(source)), target += 64, source += 64, body_length -= 64;
+        _mm512_mask_storeu_epi8(target, tail_mask, _mm512_maskz_loadu_epi8(tail_mask, source));
+    }
+    else {
+        // Tail, body, and head.
+        _mm512_mask_storeu_epi8(target + head_length + body_length, head_mask,
+                                _mm512_maskz_loadu_epi8(head_mask, source + head_length + body_length));
+
+        // Define the permutation vectors for the `permute2var` instruction for the body.
+        sz_u512_vec_t first_vec, second_vec, combined_vec, selector_vec;
+        selector_vec.zmm = _mm512_set_epi8(                                 //
+            63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, //
+            47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, //
+            31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, //
+            15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
+        selector_vec.zmm = _mm512_add_epi8(selector_vec.zmm, _mm512_set1_epi8(shift_in_page));
+        selector_vec.zmm = _mm512_and_si512(selector_vec.zmm, _mm512_set1_epi8(63));
+
+        if (body_length >= 128) {
+            // Now that the permutations are prepared, pre-load the first cache line and start the loop.
+            __mmask64 blend_mask = _sz_u64_mask_until(shift_in_page);
+            sz_cptr_t source_second_page = source + body_length - (sz_size_t)(source + body_length) % 64;
+            first_vec.zmm = _mm512_load_si512(source_second_page);
+            for (; body_length >= 128; source_second_page -= 64, body_length -= 64) {
+                second_vec.zmm = _mm512_load_si512(source_second_page - 64);
+                second_vec.zmm = _mm512_permutexvar_epi8(selector_vec.zmm, second_vec.zmm);
+                combined_vec.zmm = _mm512_mask_blend_epi8(blend_mask, second_vec.zmm, first_vec.zmm);
+                sz_assert(combined_vec.u8s[0] == source[0]);
+                sz_assert(combined_vec.u8s[63] == source[63]);
+                _mm512_store_si512(target + head_length + body_length, combined_vec.zmm);
+                first_vec.zmm = second_vec.zmm;
+            }
+        }
+        if (body_length) _mm512_store_si512(target + head_length, _mm512_loadu_si512(source + head_length));
+        _mm512_mask_storeu_epi8(target, tail_mask, _mm512_maskz_loadu_epi8(tail_mask, source));
+    }
+}
\ No newline at end of file
diff --git a/include/stringzilla/stringzilla.h b/include/stringzilla/stringzilla.h
index 291dbf9c..0068c11f 100644
--- a/include/stringzilla/stringzilla.h
+++ b/include/stringzilla/stringzilla.h
@@ -453,6 +453,25 @@ SZ_DYNAMIC sz_ordering_t sz_order(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b,
 /** @copydoc sz_order */
 SZ_PUBLIC sz_ordering_t sz_order_serial(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length);
 
+/**
+ *  @brief  Look Up Table @b (LUT) transformation of a string. Equivalent to `for (char & c : text) c = lut[c]`.
+ *
+ *  Can be used to implement some form of string normalization, partially masking punctuation marks,
+ *  or converting between different character sets, like uppercase or lowercase. Surprisingly, also has
+ *  broad implications in image processing, where image channel transformations are often done using LUTs.
+ *
+ *  @param text     String to be normalized.
+ *  @param length   Number of bytes in the string.
+ *  @param lut      Look Up Table to apply. Must be exactly @b 256 bytes long.
+ *  @param result   Output string, can point to the same address as ::text.
+ */
+SZ_DYNAMIC void sz_look_up_transform(sz_cptr_t text, sz_size_t length, sz_cptr_t lut, sz_ptr_t result);
+
+typedef void (*sz_look_up_transform_t)(sz_cptr_t, sz_size_t, sz_cptr_t, sz_ptr_t);
+
+/** @copydoc sz_look_up_transform */
+SZ_PUBLIC void sz_look_up_transform_serial(sz_cptr_t text, sz_size_t length, sz_cptr_t lut, sz_ptr_t result);
+
 /**
  *  @brief  Equivalent to `for (char & c : text) c = tolower(c)`.
  *
@@ -1159,16 +1178,18 @@ SZ_PUBLIC void sz_sort_intro(sz_sequence_t *sequence, sz_sequence_comparator_t l
 
 #if SZ_USE_X86_AVX512
 
-/** @copydoc sz_equal_serial */
+/** @copydoc sz_equal */
 SZ_PUBLIC sz_bool_t sz_equal_avx512(sz_cptr_t a, sz_cptr_t b, sz_size_t length);
-/** @copydoc sz_order_serial */
+/** @copydoc sz_order */
 SZ_PUBLIC sz_ordering_t sz_order_avx512(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length);
-/** @copydoc sz_copy_serial */
+/** @copydoc sz_copy */
 SZ_PUBLIC void sz_copy_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
-/** @copydoc sz_move_serial */
+/** @copydoc sz_move */
 SZ_PUBLIC void sz_move_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
-/** @copydoc sz_fill_serial */
+/** @copydoc sz_fill */
 SZ_PUBLIC void sz_fill_avx512(sz_ptr_t target, sz_size_t length, sz_u8_t value);
+/** @copydoc sz_look_up_tranform */
+SZ_PUBLIC void sz_look_up_tranform_avx512(sz_cptr_t source, sz_size_t length, sz_cptr_t table, sz_ptr_t target);
 /** @copydoc sz_find_byte */
 SZ_PUBLIC sz_cptr_t sz_find_byte_avx512(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle);
 /** @copydoc sz_rfind_byte */
@@ -1194,12 +1215,18 @@ SZ_PUBLIC void sz_hashes_avx512(sz_cptr_t text, sz_size_t length, sz_size_t wind
 #endif
 
 #if SZ_USE_X86_AVX2
+/** @copydoc sz_equal */
+SZ_PUBLIC sz_bool_t sz_equal_avx2(sz_cptr_t a, sz_cptr_t b, sz_size_t length);
+/** @copydoc sz_order */
+SZ_PUBLIC sz_ordering_t sz_order_avx2(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length);
 /** @copydoc sz_copy */
 SZ_PUBLIC void sz_copy_avx2(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
 /** @copydoc sz_move */
 SZ_PUBLIC void sz_move_avx2(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
 /** @copydoc sz_fill */
 SZ_PUBLIC void sz_fill_avx2(sz_ptr_t target, sz_size_t length, sz_u8_t value);
+/** @copydoc sz_look_up_transform */
+SZ_PUBLIC void sz_look_up_transform_avx2(sz_cptr_t source, sz_size_t length, sz_cptr_t table, sz_ptr_t target);
 /** @copydoc sz_find_byte */
 SZ_PUBLIC sz_cptr_t sz_find_byte_avx2(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle);
 /** @copydoc sz_rfind_byte */
@@ -1216,6 +1243,16 @@ SZ_PUBLIC void sz_hashes_avx2(sz_cptr_t text, sz_size_t length, sz_size_t window
 #if SZ_USE_ARM_NEON
 /** @copydoc sz_equal */
 SZ_PUBLIC sz_bool_t sz_equal_neon(sz_cptr_t a, sz_cptr_t b, sz_size_t length);
+/** @copydoc sz_order */
+SZ_PUBLIC sz_ordering_t sz_order_neon(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length);
+/** @copydoc sz_copy */
+SZ_PUBLIC void sz_copy_neon(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
+/** @copydoc sz_move */
+SZ_PUBLIC void sz_move_neon(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
+/** @copydoc sz_fill */
+SZ_PUBLIC void sz_fill_neon(sz_ptr_t target, sz_size_t length, sz_u8_t value);
+/** @copydoc sz_look_up_transform */
+SZ_PUBLIC void sz_look_up_transform_neon(sz_cptr_t source, sz_size_t length, sz_cptr_t table, sz_ptr_t target);
 /** @copydoc sz_find_byte */
 SZ_PUBLIC sz_cptr_t sz_find_byte_neon(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle);
 /** @copydoc sz_rfind_byte */
@@ -1230,6 +1267,31 @@ SZ_PUBLIC sz_cptr_t sz_find_charset_neon(sz_cptr_t text, sz_size_t length, sz_ch
 SZ_PUBLIC sz_cptr_t sz_rfind_charset_neon(sz_cptr_t text, sz_size_t length, sz_charset_t const *set);
 #endif
 
+#if SZ_USE_ARM_SVE
+/** @copydoc sz_equal */
+SZ_PUBLIC sz_bool_t sz_equal_sve(sz_cptr_t a, sz_cptr_t b, sz_size_t length);
+/** @copydoc sz_order */
+SZ_PUBLIC sz_ordering_t sz_order_sve(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length);
+/** @copydoc sz_copy */
+SZ_PUBLIC void sz_copy_sve(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
+/** @copydoc sz_move */
+SZ_PUBLIC void sz_move_sve(sz_ptr_t target, sz_cptr_t source, sz_size_t length);
+/** @copydoc sz_fill */
+SZ_PUBLIC void sz_fill_sve(sz_ptr_t target, sz_size_t length, sz_u8_t value);
+/** @copydoc sz_find_byte */
+SZ_PUBLIC sz_cptr_t sz_find_byte_sve(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle);
+/** @copydoc sz_rfind_byte */
+SZ_PUBLIC sz_cptr_t sz_rfind_byte_sve(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle);
+/** @copydoc sz_find */
+SZ_PUBLIC sz_cptr_t sz_find_sve(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle, sz_size_t n_length);
+/** @copydoc sz_rfind */
+SZ_PUBLIC sz_cptr_t sz_rfind_sve(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle, sz_size_t n_length);
+/** @copydoc sz_find_charset */
+SZ_PUBLIC sz_cptr_t sz_find_charset_sve(sz_cptr_t text, sz_size_t length, sz_charset_t const *set);
+/** @copydoc sz_rfind_charset */
+SZ_PUBLIC sz_cptr_t sz_rfind_charset_sve(sz_cptr_t text, sz_size_t length, sz_charset_t const *set);
+#endif
+
 #pragma endregion
 
 #pragma GCC diagnostic push
@@ -1289,12 +1351,13 @@ SZ_PUBLIC sz_cptr_t sz_rfind_charset_neon(sz_cptr_t text, sz_size_t length, sz_c
 #if SZ_DEBUG && defined(SZ_AVOID_LIBC) && !SZ_AVOID_LIBC && !defined(SZ_PIC)
 #include <stdio.h>  // `fprintf`
 #include <stdlib.h> // `EXIT_FAILURE`
-#define sz_assert(condition)                                                                                \
-    do {                                                                                                    \
-        if (!(condition)) {                                                                                 \
-            fprintf(stderr, "Assertion failed: %s, in file %s, line %d\n", #condition, __FILE__, __LINE__); \
-            exit(EXIT_FAILURE);                                                                             \
-        }                                                                                                   \
+SZ_PUBLIC void _sz_assert_failure(char const *condition, char const *file, int line) {
+    fprintf(stderr, "Assertion failed: %s, in file %s, line %d\n", condition, file, line);
+    exit(EXIT_FAILURE);
+}
+#define sz_assert(condition)                                                      \
+    do {                                                                          \
+        if (!(condition)) { _sz_assert_failure(#condition, __FILE__, __LINE__); } \
     } while (0)
 #else
 #define sz_assert(condition) ((void)(condition))
@@ -3063,6 +3126,14 @@ SZ_INTERNAL sz_u8_t sz_u8_divide(sz_u8_t number, sz_u8_t divisor) {
     return (sz_u8_t)(t >> shift);
 }
 
+SZ_PUBLIC void sz_look_up_transform_serial(sz_cptr_t text, sz_size_t length, sz_cptr_t lut, sz_ptr_t result) {
+    sz_u8_t const *unsigned_lut = (sz_u8_t const *)lut;
+    sz_u8_t const *unsigned_text = (sz_u8_t const *)text;
+    sz_u8_t *unsigned_result = (sz_u8_t *)result;
+    sz_u8_t const *end = unsigned_text + length;
+    for (; unsigned_text != end; ++unsigned_text, ++unsigned_result) *unsigned_result = unsigned_lut[*unsigned_text];
+}
+
 SZ_PUBLIC void sz_tolower_serial(sz_cptr_t text, sz_size_t length, sz_ptr_t result) {
     sz_u8_t *unsigned_result = (sz_u8_t *)result;
     sz_u8_t const *unsigned_text = (sz_u8_t const *)text;
@@ -3738,15 +3809,135 @@ typedef union sz_u256_vec_t {
     sz_u8_t u8s[32];
 } sz_u256_vec_t;
 
+SZ_PUBLIC sz_ordering_t sz_order_avx2(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length) {
+    //! Before optimizing this, read the "Operations Not Worth Optimizing" in Contributions Guide:
+    //! https://github.com/ashvardanian/StringZilla/blob/main/CONTRIBUTING.md#general-performance-observations
+    return sz_order_serial(a, a_length, b, b_length);
+}
+
+SZ_PUBLIC sz_bool_t sz_equal_avx2(sz_cptr_t a, sz_cptr_t b, sz_size_t length) {
+    sz_u256_vec_t a_vec, b_vec;
+
+    while (length >= 32) {
+        a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
+        b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
+        // One approach can be to use "movemasks", but we could also use a bitwise matching like `_mm256_testnzc_si256`.
+        int difference_mask = ~_mm256_movemask_epi8(_mm256_cmpeq_epi8(a_vec.ymm, b_vec.ymm));
+        if (difference_mask == 0) { a += 32, b += 32, length -= 32; }
+        else { return sz_false_k; }
+    }
+
+    if (length) return sz_equal_serial(a, b, length);
+    return sz_true_k;
+}
+
 SZ_PUBLIC void sz_fill_avx2(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
-    for (; length >= 32; target += 32, length -= 32) _mm256_storeu_si256((__m256i *)target, _mm256_set1_epi8(value));
-    sz_fill_serial(target, length, value);
+    char value_char = *(char *)&value;
+    __m256i value_vec = _mm256_set1_epi8(value_char);
+    // The naive implementation of this function is very simple.
+    // It assumes the CPU is great at handling unaligned "stores".
+    //
+    //    for (; length >= 32; target += 32, length -= 32) _mm256_storeu_si256(target, value_vec);
+    //    sz_fill_serial(target, length, value);
+    //
+    // When the buffer is small, there isn't much to innovate.
+    if (length <= 32) sz_fill_serial(target, length, value);
+    // When the buffer is aligned, we can avoid any split-stores.
+    else {
+        sz_size_t head_length = (32 - ((sz_size_t)target % 32)) % 32; // 31 or less.
+        sz_size_t tail_length = (sz_size_t)(target + length) % 32;    // 31 or less.
+        sz_size_t body_length = length - head_length - tail_length;   // Multiple of 32.
+        sz_u16_t value16 = (sz_u16_t)value * 0x0101u;
+        sz_u32_t value32 = (sz_u32_t)value16 * 0x00010001u;
+        sz_u64_t value64 = (sz_u64_t)value32 * 0x0000000100000001ull;
+
+        // Fill the head of the buffer. This part is much cleaner with AVX-512.
+        if (head_length & 1) *(sz_u8_t *)target = value, target++, head_length--;
+        if (head_length & 2) *(sz_u16_t *)target = value16, target += 2, head_length -= 2;
+        if (head_length & 4) *(sz_u32_t *)target = value32, target += 4, head_length -= 4;
+        if (head_length & 8) *(sz_u64_t *)target = value64, target += 8, head_length -= 8;
+        if (head_length & 16)
+            _mm_store_si128((__m128i *)target, _mm_set1_epi8(value_char)), target += 16, head_length -= 16;
+        sz_assert((sz_size_t)target % 32 == 0 && "Target is supposed to be aligned to the YMM register size.");
+
+        // Fill the aligned body of the buffer.
+        for (; body_length >= 32; target += 32, body_length -= 32) _mm256_store_si256((__m256i *)target, value_vec);
+
+        // Fill the tail of the buffer. This part is much cleaner with AVX-512.
+        sz_assert((sz_size_t)target % 32 == 0 && "Target is supposed to be aligned to the YMM register size.");
+        if (tail_length & 16)
+            _mm_store_si128((__m128i *)target, _mm_set1_epi8(value_char)), target += 16, tail_length -= 16;
+        if (tail_length & 8) *(sz_u64_t *)target = value64, target += 8, tail_length -= 8;
+        if (tail_length & 4) *(sz_u32_t *)target = value32, target += 4, tail_length -= 4;
+        if (tail_length & 2) *(sz_u16_t *)target = value16, target += 2, tail_length -= 2;
+        if (tail_length & 1) *(sz_u8_t *)target = value, target++, tail_length--;
+    }
 }
 
 SZ_PUBLIC void sz_copy_avx2(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
-    for (; length >= 32; target += 32, source += 32, length -= 32)
-        _mm256_storeu_si256((__m256i *)target, _mm256_lddqu_si256((__m256i const *)source));
-    sz_copy_serial(target, source, length);
+    // The naive implementation of this function is very simple.
+    // It assumes the CPU is great at handling unaligned "stores" and "loads".
+    //
+    //    for (; length >= 32; target += 32, source += 32, length -= 32)
+    //        _mm256_storeu_si256((__m256i *)target, _mm256_lddqu_si256((__m256i const *)source));
+    //    sz_copy_serial(target, source, length);
+    //
+    // A typical AWS Skylake instance can have 32 KB x 2 blocks of L1 data cache per core,
+    // 1 MB x 2 blocks of L2 cache per core, and one shared L3 cache buffer.
+    // For now, let's avoid the cases beyond the L2 size.
+    int is_huge = length > 1ull * 1024ull * 1024ull;
+    if (length <= 32) { sz_copy_serial(target, source, length); }
+    // When dealing wirh larger arrays, the optimization is not as simple as with the `sz_fill_avx2` function,
+    // as both buffers may be unaligned. If we are lucky and the requested operation is some huge page transfer,
+    // we can use aligned loads and stores, and the performance will be great.
+    else if ((sz_size_t)target % 32 == 0 && (sz_size_t)source % 32 == 0 && !is_huge) {
+        for (; length >= 32; target += 32, source += 32, length -= 32)
+            _mm256_store_si256((__m256i *)target, _mm256_load_si256((__m256i const *)source));
+        if (length) sz_copy_serial(target, source, length);
+    }
+    // The trickiest case is when both `source` and `target` are not aligned.
+    // In such and simpler cases we can copy enough bytes into `target` to reach its cacheline boundary,
+    // and then combine unaligned loads with aligned stores.
+    else {
+        sz_size_t head_length = (32 - ((sz_size_t)target % 32)) % 32; // 31 or less.
+        sz_size_t tail_length = (sz_size_t)(target + length) % 32;    // 31 or less.
+        sz_size_t body_length = length - head_length - tail_length;   // Multiple of 32.
+
+        // Fill the head of the buffer. This part is much cleaner with AVX-512.
+        if (head_length & 1) *(sz_u8_t *)target = *(sz_u8_t *)source, target++, source++, head_length--;
+        if (head_length & 2) *(sz_u16_t *)target = *(sz_u16_t *)source, target += 2, source += 2, head_length -= 2;
+        if (head_length & 4) *(sz_u32_t *)target = *(sz_u32_t *)source, target += 4, source += 4, head_length -= 4;
+        if (head_length & 8) *(sz_u64_t *)target = *(sz_u64_t *)source, target += 8, source += 8, head_length -= 8;
+        if (head_length & 16)
+            _mm_store_si128((__m128i *)target, _mm_lddqu_si128((__m128i const *)source)), target += 16, source += 16,
+                head_length -= 16;
+        sz_assert((sz_size_t)target % 32 == 0 && "Target is supposed to be aligned to the YMM register size.");
+
+        // Fill the aligned body of the buffer.
+        if (!is_huge) {
+            for (; body_length >= 32; target += 32, source += 32, body_length -= 32)
+                _mm256_store_si256((__m256i *)target, _mm256_lddqu_si256((__m256i const *)source));
+        }
+        // When the biffer is huge, we can traverse it in 2 directions.
+        else {
+            for (; body_length >= 64; target += 32, source += 32, body_length -= 64) {
+                _mm256_store_si256((__m256i *)(target), _mm256_lddqu_si256((__m256i const *)(source)));
+                _mm256_store_si256((__m256i *)(target + body_length - 32),
+                                   _mm256_lddqu_si256((__m256i const *)(source + body_length - 32)));
+            }
+            if (body_length) _mm256_store_si256((__m256i *)target, _mm256_lddqu_si256((__m256i const *)source));
+        }
+
+        // Fill the tail of the buffer. This part is much cleaner with AVX-512.
+        sz_assert((sz_size_t)target % 32 == 0 && "Target is supposed to be aligned to the YMM register size.");
+        if (tail_length & 16)
+            _mm_store_si128((__m128i *)target, _mm_lddqu_si128((__m128i const *)source)), target += 16, source += 16,
+                tail_length -= 16;
+        if (tail_length & 8) *(sz_u64_t *)target = *(sz_u64_t *)source, target += 8, source += 8, tail_length -= 8;
+        if (tail_length & 4) *(sz_u32_t *)target = *(sz_u32_t *)source, target += 4, source += 4, tail_length -= 4;
+        if (tail_length & 2) *(sz_u16_t *)target = *(sz_u16_t *)source, target += 2, source += 2, tail_length -= 2;
+        if (tail_length & 1) *(sz_u8_t *)target = *(sz_u8_t *)source, target++, source++, tail_length--;
+    }
 }
 
 SZ_PUBLIC void sz_move_avx2(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
@@ -3763,6 +3954,142 @@ SZ_PUBLIC void sz_move_avx2(sz_ptr_t target, sz_cptr_t source, sz_size_t length)
     }
 }
 
+SZ_PUBLIC void sz_look_up_transform_avx2(sz_cptr_t source, sz_size_t length, sz_cptr_t lut, sz_ptr_t target) {
+
+    // If the input is tiny (especially smaller than the look-up table itself), we may end up paying
+    // more for organizing the SIMD registers and changing the CPU state, than for the actual computation.
+    // But if at least 3 cache lines are touched, the AVX-2 implementation should be faster.
+    if (length <= 128) {
+        sz_look_up_transform_serial(source, length, lut, target);
+        return;
+    }
+
+    // We need to pull the lookup table into 8x YMM registers.
+    // The biggest issue is reorganizing the data in the lookup table, as AVX2 doesn't have 256-bit shuffle,
+    // it only has 128-bit "within-lane" shuffle. Still, it's wiser to use full YMM registers, instead of XMM,
+    // so that we can at least compensate high latency with twice larger window and one more level of lookup.
+    sz_u256_vec_t lut_0_to_15_vec, lut_16_to_31_vec, lut_32_to_47_vec, lut_48_to_63_vec, //
+        lut_64_to_79_vec, lut_80_to_95_vec, lut_96_to_111_vec, lut_112_to_127_vec,       //
+        lut_128_to_143_vec, lut_144_to_159_vec, lut_160_to_175_vec, lut_176_to_191_vec,  //
+        lut_192_to_207_vec, lut_208_to_223_vec, lut_224_to_239_vec, lut_240_to_255_vec;
+
+    lut_0_to_15_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut)));
+    lut_16_to_31_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 16)));
+    lut_32_to_47_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 32)));
+    lut_48_to_63_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 48)));
+    lut_64_to_79_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 64)));
+    lut_80_to_95_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 80)));
+    lut_96_to_111_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 96)));
+    lut_112_to_127_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 112)));
+    lut_128_to_143_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 128)));
+    lut_144_to_159_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 144)));
+    lut_160_to_175_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 160)));
+    lut_176_to_191_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 176)));
+    lut_192_to_207_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 192)));
+    lut_208_to_223_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 208)));
+    lut_224_to_239_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 224)));
+    lut_240_to_255_vec.ymm = _mm256_broadcastsi128_si256(_mm_lddqu_si128((__m128i const *)(lut + 240)));
+
+    // Assuming each lookup is performed within 16 elements of 256, we need to reduce the scope by 16x = 2^4.
+    sz_u256_vec_t not_first_bit_vec, not_second_bit_vec, not_third_bit_vec, not_fourth_bit_vec;
+
+    /// Top and bottom nibbles of the source are used separately.
+    sz_u256_vec_t source_vec, source_bot_vec;
+    sz_u256_vec_t blended_0_to_31_vec, blended_32_to_63_vec, blended_64_to_95_vec, blended_96_to_127_vec,
+        blended_128_to_159_vec, blended_160_to_191_vec, blended_192_to_223_vec, blended_224_to_255_vec;
+
+    // Handling the head.
+    while (length >= 32) {
+        // Load and separate the nibbles of each byte in the source.
+        source_vec.ymm = _mm256_lddqu_si256((__m256i const *)source);
+        source_bot_vec.ymm = _mm256_and_si256(source_vec.ymm, _mm256_set1_epi8((char)0x0F));
+
+        // In the first round, we select using the 4th bit.
+        not_fourth_bit_vec.ymm = _mm256_cmpeq_epi8( //
+            _mm256_and_si256(_mm256_set1_epi8((char)0x10), source_vec.ymm), _mm256_setzero_si256());
+        blended_0_to_31_vec.ymm = _mm256_blendv_epi8(                      //
+            _mm256_shuffle_epi8(lut_16_to_31_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_0_to_15_vec.ymm, source_bot_vec.ymm),  //
+            not_fourth_bit_vec.ymm);
+        blended_32_to_63_vec.ymm = _mm256_blendv_epi8(                     //
+            _mm256_shuffle_epi8(lut_48_to_63_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_32_to_47_vec.ymm, source_bot_vec.ymm), //
+            not_fourth_bit_vec.ymm);
+        blended_64_to_95_vec.ymm = _mm256_blendv_epi8(                     //
+            _mm256_shuffle_epi8(lut_80_to_95_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_64_to_79_vec.ymm, source_bot_vec.ymm), //
+            not_fourth_bit_vec.ymm);
+        blended_96_to_127_vec.ymm = _mm256_blendv_epi8(                      //
+            _mm256_shuffle_epi8(lut_112_to_127_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_96_to_111_vec.ymm, source_bot_vec.ymm),  //
+            not_fourth_bit_vec.ymm);
+        blended_128_to_159_vec.ymm = _mm256_blendv_epi8(                     //
+            _mm256_shuffle_epi8(lut_144_to_159_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_128_to_143_vec.ymm, source_bot_vec.ymm), //
+            not_fourth_bit_vec.ymm);
+        blended_160_to_191_vec.ymm = _mm256_blendv_epi8(                     //
+            _mm256_shuffle_epi8(lut_176_to_191_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_160_to_175_vec.ymm, source_bot_vec.ymm), //
+            not_fourth_bit_vec.ymm);
+        blended_192_to_223_vec.ymm = _mm256_blendv_epi8(                     //
+            _mm256_shuffle_epi8(lut_208_to_223_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_192_to_207_vec.ymm, source_bot_vec.ymm), //
+            not_fourth_bit_vec.ymm);
+        blended_224_to_255_vec.ymm = _mm256_blendv_epi8(                     //
+            _mm256_shuffle_epi8(lut_240_to_255_vec.ymm, source_bot_vec.ymm), //
+            _mm256_shuffle_epi8(lut_224_to_239_vec.ymm, source_bot_vec.ymm), //
+            not_fourth_bit_vec.ymm);
+
+        // Perform a tree-like reduction of the 8x "blended" YMM registers, depending on the "source" content.
+        // The first round selects using the 3rd bit.
+        not_third_bit_vec.ymm = _mm256_cmpeq_epi8( //
+            _mm256_and_si256(_mm256_set1_epi8((char)0x20), source_vec.ymm), _mm256_setzero_si256());
+        blended_0_to_31_vec.ymm = _mm256_blendv_epi8( //
+            blended_32_to_63_vec.ymm,                 //
+            blended_0_to_31_vec.ymm,                  //
+            not_third_bit_vec.ymm);
+        blended_64_to_95_vec.ymm = _mm256_blendv_epi8( //
+            blended_96_to_127_vec.ymm,                 //
+            blended_64_to_95_vec.ymm,                  //
+            not_third_bit_vec.ymm);
+        blended_128_to_159_vec.ymm = _mm256_blendv_epi8( //
+            blended_160_to_191_vec.ymm,                  //
+            blended_128_to_159_vec.ymm,                  //
+            not_third_bit_vec.ymm);
+        blended_192_to_223_vec.ymm = _mm256_blendv_epi8( //
+            blended_224_to_255_vec.ymm,                  //
+            blended_192_to_223_vec.ymm,                  //
+            not_third_bit_vec.ymm);
+
+        // The second round selects using the 2nd bit.
+        not_second_bit_vec.ymm = _mm256_cmpeq_epi8( //
+            _mm256_and_si256(_mm256_set1_epi8((char)0x40), source_vec.ymm), _mm256_setzero_si256());
+        blended_0_to_31_vec.ymm = _mm256_blendv_epi8( //
+            blended_64_to_95_vec.ymm,                 //
+            blended_0_to_31_vec.ymm,                  //
+            not_second_bit_vec.ymm);
+        blended_128_to_159_vec.ymm = _mm256_blendv_epi8( //
+            blended_192_to_223_vec.ymm,                  //
+            blended_128_to_159_vec.ymm,                  //
+            not_second_bit_vec.ymm);
+
+        // The third round selects using the 1st bit.
+        not_first_bit_vec.ymm = _mm256_cmpeq_epi8( //
+            _mm256_and_si256(_mm256_set1_epi8((char)0x80), source_vec.ymm), _mm256_setzero_si256());
+        blended_0_to_31_vec.ymm = _mm256_blendv_epi8( //
+            blended_128_to_159_vec.ymm,               //
+            blended_0_to_31_vec.ymm,                  //
+            not_first_bit_vec.ymm);
+
+        // And dump the result into the target.
+        _mm256_storeu_si256((__m256i *)target, blended_0_to_31_vec.ymm);
+        source += 32, target += 32, length -= 32;
+    }
+
+    // Handle the tail.
+    if (length) sz_look_up_transform_serial(source, length, lut, target);
+}
+
 SZ_PUBLIC sz_cptr_t sz_find_byte_avx2(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n) {
     int mask;
     sz_u256_vec_t h_vec, n_vec;
@@ -4160,17 +4487,38 @@ SZ_INTERNAL __mmask64 _sz_u64_mask_until(sz_size_t n) {
 
 SZ_PUBLIC sz_ordering_t sz_order_avx512(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length) {
     sz_u512_vec_t a_vec, b_vec;
-    __mmask64 a_mask, b_mask, mask_not_equal;
+
+    // Pointer arithmetic is cheap, fetching memory is not!
+    // So we can use the masked loads to fetch at most one cache-line for each string,
+    // compare the prefixes, and only then move forward.
+    sz_size_t a_head_length = 64 - ((sz_size_t)a % 64); // 63 or less.
+    sz_size_t b_head_length = 64 - ((sz_size_t)b % 64); // 63 or less.
+    a_head_length = a_head_length < a_length ? a_head_length : a_length;
+    b_head_length = b_head_length < b_length ? b_head_length : b_length;
+    sz_size_t head_length = a_head_length < b_head_length ? a_head_length : b_head_length;
+    __mmask64 head_mask = _sz_u64_mask_until(head_length);
+    a_vec.zmm = _mm512_maskz_loadu_epi8(head_mask, a);
+    b_vec.zmm = _mm512_maskz_loadu_epi8(head_mask, b);
+    __mmask64 mask_not_equal = _mm512_cmpneq_epi8_mask(a_vec.zmm, b_vec.zmm);
+    if (mask_not_equal != 0) {
+        sz_u64_t first_diff = _tzcnt_u64(mask_not_equal);
+        char a_char = a_vec.u8s[first_diff];
+        char b_char = b_vec.u8s[first_diff];
+        return _sz_order_scalars(a_char, b_char);
+    }
+    else if (head_length == a_length && head_length == b_length) { return sz_equal_k; }
+    else { a += head_length, b += head_length, a_length -= head_length, b_length -= head_length; }
 
     // The rare case, when both string are very long.
+    __mmask64 a_mask, b_mask;
     while ((a_length >= 64) & (b_length >= 64)) {
         a_vec.zmm = _mm512_loadu_si512(a);
         b_vec.zmm = _mm512_loadu_si512(b);
         mask_not_equal = _mm512_cmpneq_epi8_mask(a_vec.zmm, b_vec.zmm);
         if (mask_not_equal != 0) {
             sz_u64_t first_diff = _tzcnt_u64(mask_not_equal);
-            char a_char = a[first_diff];
-            char b_char = b[first_diff];
+            char a_char = a_vec.u8s[first_diff];
+            char b_char = b_vec.u8s[first_diff];
             return _sz_order_scalars(a_char, b_char);
         }
         a += 64, b += 64, a_length -= 64, b_length -= 64;
@@ -4188,17 +4536,16 @@ SZ_PUBLIC sz_ordering_t sz_order_avx512(sz_cptr_t a, sz_size_t a_length, sz_cptr
         mask_not_equal = _mm512_cmpneq_epi8_mask(a_vec.zmm, b_vec.zmm);
         if (mask_not_equal != 0) {
             sz_u64_t first_diff = _tzcnt_u64(mask_not_equal);
-            char a_char = a[first_diff];
-            char b_char = b[first_diff];
+            char a_char = a_vec.u8s[first_diff];
+            char b_char = b_vec.u8s[first_diff];
             return _sz_order_scalars(a_char, b_char);
         }
-        else
-            // From logic perspective, the hardest cases are "abc\0" and "abc".
-            // The result must be `sz_greater_k`, as the latter is shorter.
-            return _sz_order_scalars(a_length, b_length);
+        // From logic perspective, the hardest cases are "abc\0" and "abc".
+        // The result must be `sz_greater_k`, as the latter is shorter.
+        else { return _sz_order_scalars(a_length, b_length); }
     }
-    else
-        return sz_equal_k;
+
+    return sz_equal_k;
 }
 
 SZ_PUBLIC sz_bool_t sz_equal_avx512(sz_cptr_t a, sz_cptr_t b, sz_size_t length) {
@@ -4221,39 +4568,220 @@ SZ_PUBLIC sz_bool_t sz_equal_avx512(sz_cptr_t a, sz_cptr_t b, sz_size_t length)
         mask = _mm512_mask_cmpneq_epi8_mask(mask, a_vec.zmm, b_vec.zmm);
         return (sz_bool_t)(mask == 0);
     }
-    else
-        return sz_true_k;
+
+    return sz_true_k;
 }
 
 SZ_PUBLIC void sz_fill_avx512(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
-    for (; length >= 64; target += 64, length -= 64) _mm512_storeu_si512(target, _mm512_set1_epi8(value));
-    // At this point the length is guaranteed to be under 64.
-    _mm512_mask_storeu_epi8(target, _sz_u64_mask_until(length), _mm512_set1_epi8(value));
+    __m512i value_vec = _mm512_set1_epi8(value);
+    // The naive implementation of this function is very simple.
+    // It assumes the CPU is great at handling unaligned "stores".
+    //
+    //    for (; length >= 64; target += 64, length -= 64) _mm512_storeu_si512(target, value_vec);
+    //    _mm512_mask_storeu_epi8(target, _sz_u64_mask_until(length), value_vec);
+    //
+    // When the buffer is small, there isn't much to innovate.
+    if (length <= 64) {
+        __mmask64 mask = _sz_u64_mask_until(length);
+        _mm512_mask_storeu_epi8(target, mask, value_vec);
+    }
+    // When the buffer is over 64 bytes, it's guaranteed to touch at least two cache lines - the head and tail,
+    // and may include more cache-lines in-between. Knowing this, we can avoid expensive unaligned stores
+    // by computing 2 masks - for the head and tail, using masked stores for the head and tail, and unmasked
+    // for the body.
+    else {
+        sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64; // 63 or less.
+        sz_size_t tail_length = (sz_size_t)(target + length) % 64;    // 63 or less.
+        sz_size_t body_length = length - head_length - tail_length;   // Multiple of 64.
+        __mmask64 head_mask = _sz_u64_mask_until(head_length);
+        __mmask64 tail_mask = _sz_u64_mask_until(tail_length);
+        _mm512_mask_storeu_epi8(target, head_mask, value_vec);
+        for (target += head_length; body_length >= 64; target += 64, body_length -= 64)
+            _mm512_store_si512(target, value_vec);
+        _mm512_mask_storeu_epi8(target, tail_mask, value_vec);
+    }
 }
 
 SZ_PUBLIC void sz_copy_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
-    for (; length >= 64; target += 64, source += 64, length -= 64)
-        _mm512_storeu_si512(target, _mm512_loadu_si512(source));
-    // At this point the length is guaranteed to be under 64.
-    __mmask64 mask = _sz_u64_mask_until(length);
-    _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
-}
+    // The naive implementation of this function is very simple.
+    // It assumes the CPU is great at handling unaligned "stores" and "loads".
+    //
+    //    for (; length >= 64; target += 64, source += 64, length -= 64)
+    //        _mm512_storeu_si512(target, _mm512_loadu_si512(source));
+    //    __mmask64 mask = _sz_u64_mask_until(length);
+    //    _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
+    //
+    // A typical AWS Sapphire Rapids instance can have 48 KB x 2 blocks of L1 data cache per core,
+    // 2 MB x 2 blocks of L2 cache per core, and one shared 60 MB buffer of L3 cache.
+    // With two strings, we may consider the overal workload huge, if each exceeds 1 MB in length.
+    int const is_huge = length >= 1ull * 1024ull * 1024ull;
 
-SZ_PUBLIC void sz_move_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
-    if (target < source || target >= source + length) {
+    // When the buffer is small, there isn't much to innovate.
+    if (length <= 64) {
+        __mmask64 mask = _sz_u64_mask_until(length);
+        _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
+    }
+    // When dealing wirh larger arrays, the optimization is not as simple as with the `sz_fill_avx512` function,
+    // as both buffers may be unaligned. If we are lucky and the requested operation is some huge page transfer,
+    // we can use aligned loads and stores, and the performance will be great.
+    else if ((sz_size_t)target % 64 == 0 && (sz_size_t)source % 64 == 0 && !is_huge) {
         for (; length >= 64; target += 64, source += 64, length -= 64)
-            _mm512_storeu_si512(target, _mm512_loadu_si512(source));
+            _mm512_store_si512(target, _mm512_load_si512(source));
         // At this point the length is guaranteed to be under 64.
         __mmask64 mask = _sz_u64_mask_until(length);
+        // Aligned load and stores would work too, but it's not defined.
         _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
     }
+    // The trickiest case is when both `source` and `target` are not aligned.
+    // In such and simpler cases we can copy enough bytes into `target` to reach its cacheline boundary,
+    // and then combine unaligned loads with aligned stores.
+    else if (!is_huge) {
+        sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64; // 63 or less.
+        sz_size_t tail_length = (sz_size_t)(target + length) % 64;    // 63 or less.
+        sz_size_t body_length = length - head_length - tail_length;   // Multiple of 64.
+        __mmask64 head_mask = _sz_u64_mask_until(head_length);
+        __mmask64 tail_mask = _sz_u64_mask_until(tail_length);
+        _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+        for (target += head_length, source += head_length; body_length >= 64;
+             target += 64, source += 64, body_length -= 64)
+            _mm512_store_si512(target, _mm512_loadu_si512(source)); // Unaligned load, but aligned store!
+        _mm512_mask_storeu_epi8(target, tail_mask, _mm512_maskz_loadu_epi8(tail_mask, source));
+    }
+    // For gigantic buffers, exceeding typical L1 cache sizes, there are other tricks we can use.
+    //
+    //      1. Moving in both directions to maximize the throughput, when fetching from multiple
+    //         memory pages. Also helps with cache set-associativity issues, as we won't always
+    //         be fetching the same entries in the lookup table.
+    //      2. Using non-temporal stores to avoid polluting the cache.
+    //      3. Prefetching the next cache line, to avoid stalling the CPU. This generally useless
+    //         for predictable patterns, so disregard this advice.
+    //
+    // Bidirectional traversal adds about 10%, accelerating from 11 GB/s to 12 GB/s.
+    // Using "streaming stores" boosts us from 12 GB/s to 19 GB/s.
     else {
-        // Jump to the end and walk backwards.
-        for (target += length, source += length; length >= 64; length -= 64)
-            _mm512_storeu_si512(target -= 64, _mm512_loadu_si512(source -= 64));
-        // At this point the length is guaranteed to be under 64.
+        sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64;
+        sz_size_t tail_length = (sz_size_t)(target + length) % 64;
+        sz_size_t body_length = length - head_length - tail_length;
+        __mmask64 head_mask = _sz_u64_mask_until(head_length);
+        __mmask64 tail_mask = _sz_u64_mask_until(tail_length);
+        _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+        _mm512_mask_storeu_epi8(target + head_length + body_length, tail_mask,
+                                _mm512_maskz_loadu_epi8(tail_mask, source));
+
+        // Now in the main loop, we can use non-temporal loads and stores,
+        // performing the operation in both directions.
+        for (target += head_length, source += head_length; //
+             body_length >= 128;                           //
+             target += 64, source += 64, body_length -= 128) {
+            _mm512_stream_si512((__m512i *)(target), _mm512_loadu_si512(source));
+            _mm512_stream_si512((__m512i *)(target + body_length - 64), _mm512_loadu_si512(source + body_length - 64));
+        }
+        if (body_length >= 64) _mm512_stream_si512((__m512i *)target, _mm512_loadu_si512(source));
+    }
+}
+
+SZ_PUBLIC void sz_move_avx512(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
+    if (target == source) return; // Don't be silly, don't move the data if it's already there.
+
+    // On very short buffers, that are one cache line in width or less, we don't need any loops.
+    // We can also avoid any data-dependencies between iterations, assuming we have 32 registers
+    // to pre-load the data, before writing it back.
+    if (length <= 64) {
         __mmask64 mask = _sz_u64_mask_until(length);
-        _mm512_mask_storeu_epi8(target - length, mask, _mm512_maskz_loadu_epi8(mask, source - length));
+        _mm512_mask_storeu_epi8(target, mask, _mm512_maskz_loadu_epi8(mask, source));
+    }
+    else if (length <= 128) {
+        sz_size_t last_length = length - 64;
+        __mmask64 mask = _sz_u64_mask_until(last_length);
+        __m512i source0 = _mm512_loadu_epi8(source);
+        __m512i source1 = _mm512_maskz_loadu_epi8(mask, source + 64);
+        _mm512_storeu_epi8(target, source0);
+        _mm512_mask_storeu_epi8(target + 64, mask, source1);
+    }
+    else if (length <= 192) {
+        sz_size_t last_length = length - 128;
+        __mmask64 mask = _sz_u64_mask_until(last_length);
+        __m512i source0 = _mm512_loadu_epi8(source);
+        __m512i source1 = _mm512_loadu_epi8(source + 64);
+        __m512i source2 = _mm512_maskz_loadu_epi8(mask, source + 128);
+        _mm512_storeu_epi8(target, source0);
+        _mm512_storeu_epi8(target + 64, source1);
+        _mm512_mask_storeu_epi8(target + 128, mask, source2);
+    }
+    else if (length <= 256) {
+        sz_size_t last_length = length - 192;
+        __mmask64 mask = _sz_u64_mask_until(last_length);
+        __m512i source0 = _mm512_loadu_epi8(source);
+        __m512i source1 = _mm512_loadu_epi8(source + 64);
+        __m512i source2 = _mm512_loadu_epi8(source + 128);
+        __m512i source3 = _mm512_maskz_loadu_epi8(mask, source + 192);
+        _mm512_storeu_epi8(target, source0);
+        _mm512_storeu_epi8(target + 64, source1);
+        _mm512_storeu_epi8(target + 128, source2);
+        _mm512_mask_storeu_epi8(target + 192, mask, source3);
+    }
+
+    // If the regions don't overlap at all, just use "copy" and save some brain cells thinking about corner cases.
+    else if (target + length < source || target >= source + length) { sz_copy_avx512(target, source, length); }
+
+    // When the buffer is over 64 bytes, it's guaranteed to touch at least two cache lines - the head and tail,
+    // and may include more cache-lines in-between. Knowing this, we can avoid expensive unaligned stores
+    // by computing 2 masks - for the head and tail, using masked stores for the head and tail, and unmasked
+    // for the body.
+    else {
+        sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64; // 63 or less.
+        sz_size_t tail_length = (sz_size_t)(target + length) % 64;    // 63 or less.
+        sz_size_t body_length = length - head_length - tail_length;   // Multiple of 64.
+        __mmask64 head_mask = _sz_u64_mask_until(head_length);
+        __mmask64 tail_mask = _sz_u64_mask_until(tail_length);
+
+        // The absolute most common case of using "moves" is shifting the data within a continuous buffer
+        // when adding a removing some values in it. In such cases, a typical shift is by 1, 2, 4, 8, 16,
+        // or 32 bytes, rarely larger. For small shifts, under the size of the ZMM register, we can use shuffles.
+        //
+        // Remember:
+        //      - if we are shifting data left, that we are traversing to the right.
+        //      - if we are shifting data right, that we are traversing to the left.
+        int const left_to_right_traversal = source > target;
+
+        // Now we guarantee, that the relative shift within registers is from 1 to 63 bytes and the output is aligned.
+        // Hopefully, we need to shift more than two ZMM registers, so we could consider `valignr` instruction.
+        // Sadly, using `_mm512_alignr_epi8` doesn't make sense, as it operates at a 128-bit granularity.
+        //
+        //      - `_mm256_alignr_epi8` shifts entire 256-bit register, but we need many of them.
+        //      - `_mm512_alignr_epi32` shifts 512-bit chunks, but only if the `shift` is a multiple of 4 bytes.
+        //      - `_mm512_alignr_epi64` shifts 512-bit chunks by 8 bytes.
+        //
+        // All of those have a latency of 1 cycle, and the shift amount must be an immediate value!
+        // For 1-byte-shift granularity, the `_mm512_permutex2var_epi8` has a latency of 6 and needs VBMI!
+        // The most efficient and broadly compatible alternative could be to use a combination of align and shuffle.
+        // A similar approach was outlined in "Byte-wise alignr in AVX512F" by Wojciech Muła.
+        // http://0x80.pl/notesen/2016-10-16-avx512-byte-alignr.html
+        //
+        // That solution, is extremely mouthful, assuming we need compile time constants for the shift amount.
+        // A cleaner one, with a latency of 3 cycles, is to use `_mm512_permutexvar_epi8` or
+        // `_mm512_mask_permutexvar_epi8`, which can be seen as combination of a cross-register shuffle and blend,
+        // and is available with VBMI. That solution is still noticeably slower than AVX2.
+        //
+        // The GLibC implementation also uses non-temporal stores for larger buffers, we don't.
+        // https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch/memmove-avx512-no-vzeroupper.S.html
+        if (left_to_right_traversal) {
+            // Head, body, and tail.
+            _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+            for (target += head_length, source += head_length; body_length >= 64;
+                 target += 64, source += 64, body_length -= 64)
+                _mm512_store_si512(target, _mm512_loadu_si512(source));
+            _mm512_mask_storeu_epi8(target, tail_mask, _mm512_maskz_loadu_epi8(tail_mask, source));
+        }
+        else {
+            // Tail, body, and head.
+            _mm512_mask_storeu_epi8(target + head_length + body_length, tail_mask,
+                                    _mm512_maskz_loadu_epi8(tail_mask, source + head_length + body_length));
+            for (; body_length >= 64; body_length -= 64)
+                _mm512_store_si512(target + head_length + body_length - 64,
+                                   _mm512_loadu_si512(source + head_length + body_length - 64));
+            _mm512_mask_storeu_epi8(target, head_mask, _mm512_maskz_loadu_epi8(head_mask, source));
+        }
     }
 }
 
@@ -4299,22 +4827,63 @@ SZ_PUBLIC sz_cptr_t sz_find_avx512(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n,
     n_last_vec.zmm = _mm512_set1_epi8(n[offset_last]);
 
     // Scan through the string.
-    for (; h_length >= n_length + 64; h += 64, h_length -= 64) {
-        h_first_vec.zmm = _mm512_loadu_si512(h + offset_first);
-        h_mid_vec.zmm = _mm512_loadu_si512(h + offset_mid);
-        h_last_vec.zmm = _mm512_loadu_si512(h + offset_last);
-        matches = _kand_mask64(_kand_mask64( // Intersect the masks
-                                   _mm512_cmpeq_epi8_mask(h_first_vec.zmm, n_first_vec.zmm),
-                                   _mm512_cmpeq_epi8_mask(h_mid_vec.zmm, n_mid_vec.zmm)),
-                               _mm512_cmpeq_epi8_mask(h_last_vec.zmm, n_last_vec.zmm));
-        while (matches) {
-            int potential_offset = sz_u64_ctz(matches);
-            if (n_length <= 3 || sz_equal_avx512(h + potential_offset, n, n_length)) return h + potential_offset;
-            matches &= matches - 1;
-        }
+    // We have several optimized versions of the lagorithm for shorter strings,
+    // but they all mimic the default case for unbounded length needles
+    if (n_length >= 64) {
+        for (; h_length >= n_length + 64; h += 64, h_length -= 64) {
+            h_first_vec.zmm = _mm512_loadu_si512(h + offset_first);
+            h_mid_vec.zmm = _mm512_loadu_si512(h + offset_mid);
+            h_last_vec.zmm = _mm512_loadu_si512(h + offset_last);
+            matches = _kand_mask64(_kand_mask64( // Intersect the masks
+                                       _mm512_cmpeq_epi8_mask(h_first_vec.zmm, n_first_vec.zmm),
+                                       _mm512_cmpeq_epi8_mask(h_mid_vec.zmm, n_mid_vec.zmm)),
+                                   _mm512_cmpeq_epi8_mask(h_last_vec.zmm, n_last_vec.zmm));
+            while (matches) {
+                int potential_offset = sz_u64_ctz(matches);
+                if (sz_equal_avx512(h + potential_offset, n, n_length)) return h + potential_offset;
+                matches &= matches - 1;
+            }
 
-        // TODO: If the last character contains a bad byte, we can reposition the start of the next iteration.
-        // This will be very helpful for very long needles.
+            // TODO: If the last character contains a bad byte, we can reposition the start of the next iteration.
+            // This will be very helpful for very long needles.
+        }
+    }
+    // If there are only 2 or 3 characters in the needle, we don't even need the nested loop.
+    else if (n_length <= 3) {
+        for (; h_length >= n_length + 64; h += 64, h_length -= 64) {
+            h_first_vec.zmm = _mm512_loadu_si512(h + offset_first);
+            h_mid_vec.zmm = _mm512_loadu_si512(h + offset_mid);
+            h_last_vec.zmm = _mm512_loadu_si512(h + offset_last);
+            matches = _kand_mask64(_kand_mask64( // Intersect the masks
+                                       _mm512_cmpeq_epi8_mask(h_first_vec.zmm, n_first_vec.zmm),
+                                       _mm512_cmpeq_epi8_mask(h_mid_vec.zmm, n_mid_vec.zmm)),
+                                   _mm512_cmpeq_epi8_mask(h_last_vec.zmm, n_last_vec.zmm));
+            if (matches) return h + sz_u64_ctz(matches);
+        }
+    }
+    // If the needle is smaller than the size of the ZMM register, we can use masked comparisons
+    // to avoid the the inner-most nested loop and compare the entire needle against a haystack
+    // slice in 3 CPU cycles.
+    else {
+        __mmask64 n_mask = _sz_u64_mask_until(n_length);
+        sz_u512_vec_t n_full_vec, h_full_vec;
+        n_full_vec.zmm = _mm512_maskz_loadu_epi8(n_mask, n);
+        for (; h_length >= n_length + 64; h += 64, h_length -= 64) {
+            h_first_vec.zmm = _mm512_loadu_si512(h + offset_first);
+            h_mid_vec.zmm = _mm512_loadu_si512(h + offset_mid);
+            h_last_vec.zmm = _mm512_loadu_si512(h + offset_last);
+            matches = _kand_mask64(_kand_mask64( // Intersect the masks
+                                       _mm512_cmpeq_epi8_mask(h_first_vec.zmm, n_first_vec.zmm),
+                                       _mm512_cmpeq_epi8_mask(h_mid_vec.zmm, n_mid_vec.zmm)),
+                                   _mm512_cmpeq_epi8_mask(h_last_vec.zmm, n_last_vec.zmm));
+            while (matches) {
+                int potential_offset = sz_u64_ctz(matches);
+                h_full_vec.zmm = _mm512_maskz_loadu_epi8(n_mask, h + potential_offset);
+                if (_mm512_mask_cmpneq_epi8_mask(n_mask, h_full_vec.zmm, n_full_vec.zmm) == 0)
+                    return h + potential_offset;
+                matches &= matches - 1;
+            }
+        }
     }
 
     // The "tail" of the function uses masked loads to process the remaining bytes.
@@ -4715,6 +5284,108 @@ SZ_PUBLIC void sz_hashes_avx512(sz_cptr_t start, sz_size_t length, sz_size_t win
 #pragma clang attribute push(__attribute__((target("avx,avx512f,avx512vl,avx512bw,avx512vbmi,avx512vbmi2,bmi,bmi2"))), \
                              apply_to = function)
 
+SZ_PUBLIC void sz_look_up_transform_avx512(sz_cptr_t source, sz_size_t length, sz_cptr_t lut, sz_ptr_t target) {
+
+    // If the input is tiny (especially smaller than the look-up table itself), we may end up paying
+    // more for organizing the SIMD registers and changing the CPU state, than for the actual computation.
+    // But if at least 3 cache lines are touched, the AVX-512 implementation should be faster.
+    if (length <= 128) {
+        sz_look_up_transform_serial(source, length, lut, target);
+        return;
+    }
+
+    // When the buffer is over 64 bytes, it's guaranteed to touch at least two cache lines - the head and tail,
+    // and may include more cache-lines in-between. Knowing this, we can avoid expensive unaligned stores
+    // by computing 2 masks - for the head and tail, using masked stores for the head and tail, and unmasked
+    // for the body.
+    sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64; // 63 or less.
+    sz_size_t tail_length = (sz_size_t)(target + length) % 64;    // 63 or less.
+    __mmask64 head_mask = _sz_u64_mask_until(head_length);
+    __mmask64 tail_mask = _sz_u64_mask_until(tail_length);
+
+    // We need to pull the lookup table into 4x ZMM registers.
+    // We can use `vpermi2b` instruction to perform the look in two ZMM registers with `_mm512_permutex2var_epi8`
+    // intrinsics, but it has a 6-cycle latency on Sapphire Rapids and requires AVX512-VBMI. Assuming we need to
+    // operate on 4 registers, it might be cleaner to use 2x separate `_mm512_permutexvar_epi8` calls.
+    // Combining the results with 2x `_mm512_test_epi8_mask` and 3x blends afterwards.
+    //
+    //  - `_mm512_mask_blend_epi8` - 1 cycle latency, and generally 2x can run in parallel.
+    //  - `_mm512_test_epi8_mask` - 3 cycles latency, same as most comparison functions in AVX-512.
+    sz_u512_vec_t lut_0_to_63_vec, lut_64_to_127_vec, lut_128_to_191_vec, lut_192_to_255_vec;
+    lut_0_to_63_vec.zmm = _mm512_loadu_si512((lut));
+    lut_64_to_127_vec.zmm = _mm512_loadu_si512((lut + 64));
+    lut_128_to_191_vec.zmm = _mm512_loadu_si512((lut + 128));
+    lut_192_to_255_vec.zmm = _mm512_loadu_si512((lut + 192));
+
+    sz_u512_vec_t first_bit_vec, second_bit_vec;
+    first_bit_vec.zmm = _mm512_set1_epi8((char)0x80);
+    second_bit_vec.zmm = _mm512_set1_epi8((char)0x40);
+
+    __mmask64 first_bit_mask, second_bit_mask;
+    sz_u512_vec_t source_vec;
+    // If the top bit is set in each word of `source_vec`, than we use `lookup_128_to_191_vec` or
+    // `lookup_192_to_255_vec`. If the second bit is set, we use `lookup_64_to_127_vec` or `lookup_192_to_255_vec`.
+    sz_u512_vec_t lookup_0_to_63_vec, lookup_64_to_127_vec, lookup_128_to_191_vec, lookup_192_to_255_vec;
+    sz_u512_vec_t blended_0_to_127_vec, blended_128_to_255_vec, blended_0_to_255_vec;
+
+    // Handling the head.
+    if (head_length) {
+        source_vec.zmm = _mm512_maskz_loadu_epi8(head_mask, source);
+        lookup_0_to_63_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_0_to_63_vec.zmm);
+        lookup_64_to_127_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_64_to_127_vec.zmm);
+        lookup_128_to_191_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_128_to_191_vec.zmm);
+        lookup_192_to_255_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_192_to_255_vec.zmm);
+        first_bit_mask = _mm512_test_epi8_mask(source_vec.zmm, first_bit_vec.zmm);
+        second_bit_mask = _mm512_test_epi8_mask(source_vec.zmm, second_bit_vec.zmm);
+        blended_0_to_127_vec.zmm =
+            _mm512_mask_blend_epi8(second_bit_mask, lookup_0_to_63_vec.zmm, lookup_64_to_127_vec.zmm);
+        blended_128_to_255_vec.zmm =
+            _mm512_mask_blend_epi8(second_bit_mask, lookup_128_to_191_vec.zmm, lookup_192_to_255_vec.zmm);
+        blended_0_to_255_vec.zmm =
+            _mm512_mask_blend_epi8(first_bit_mask, blended_0_to_127_vec.zmm, blended_128_to_255_vec.zmm);
+        _mm512_mask_storeu_epi8(target, head_mask, blended_0_to_255_vec.zmm);
+        source += head_length, target += head_length, length -= head_length;
+    }
+
+    // Handling the body in 64-byte chunks aligned to cache-line boundaries with respect to `target`.
+    while (length >= 64) {
+        source_vec.zmm = _mm512_loadu_si512(source);
+        lookup_0_to_63_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_0_to_63_vec.zmm);
+        lookup_64_to_127_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_64_to_127_vec.zmm);
+        lookup_128_to_191_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_128_to_191_vec.zmm);
+        lookup_192_to_255_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_192_to_255_vec.zmm);
+        first_bit_mask = _mm512_test_epi8_mask(source_vec.zmm, first_bit_vec.zmm);
+        second_bit_mask = _mm512_test_epi8_mask(source_vec.zmm, second_bit_vec.zmm);
+        blended_0_to_127_vec.zmm =
+            _mm512_mask_blend_epi8(second_bit_mask, lookup_0_to_63_vec.zmm, lookup_64_to_127_vec.zmm);
+        blended_128_to_255_vec.zmm =
+            _mm512_mask_blend_epi8(second_bit_mask, lookup_128_to_191_vec.zmm, lookup_192_to_255_vec.zmm);
+        blended_0_to_255_vec.zmm =
+            _mm512_mask_blend_epi8(first_bit_mask, blended_0_to_127_vec.zmm, blended_128_to_255_vec.zmm);
+        _mm512_store_si512(target, blended_0_to_255_vec.zmm); //! Aligned store, our main weapon!
+        source += 64, target += 64, length -= 64;
+    }
+
+    // Handling the tail.
+    if (tail_length) {
+        source_vec.zmm = _mm512_maskz_loadu_epi8(tail_mask, source);
+        lookup_0_to_63_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_0_to_63_vec.zmm);
+        lookup_64_to_127_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_64_to_127_vec.zmm);
+        lookup_128_to_191_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_128_to_191_vec.zmm);
+        lookup_192_to_255_vec.zmm = _mm512_permutexvar_epi8(source_vec.zmm, lut_192_to_255_vec.zmm);
+        first_bit_mask = _mm512_test_epi8_mask(source_vec.zmm, first_bit_vec.zmm);
+        second_bit_mask = _mm512_test_epi8_mask(source_vec.zmm, second_bit_vec.zmm);
+        blended_0_to_127_vec.zmm =
+            _mm512_mask_blend_epi8(second_bit_mask, lookup_0_to_63_vec.zmm, lookup_64_to_127_vec.zmm);
+        blended_128_to_255_vec.zmm =
+            _mm512_mask_blend_epi8(second_bit_mask, lookup_128_to_191_vec.zmm, lookup_192_to_255_vec.zmm);
+        blended_0_to_255_vec.zmm =
+            _mm512_mask_blend_epi8(first_bit_mask, blended_0_to_127_vec.zmm, blended_128_to_255_vec.zmm);
+        _mm512_mask_storeu_epi8(target, tail_mask, blended_0_to_255_vec.zmm);
+        source += tail_length, target += tail_length, length -= tail_length;
+    }
+}
+
 SZ_PUBLIC sz_cptr_t sz_find_charset_avx512(sz_cptr_t text, sz_size_t length, sz_charset_t const *filter) {
 
     // Before initializing the AVX-512 vectors, we may want to run the sequential code for the first few bytes.
@@ -4729,7 +5400,7 @@ SZ_PUBLIC sz_cptr_t sz_find_charset_avx512(sz_cptr_t text, sz_size_t length, sz_
     // Let's unzip even and odd elements and replicate them into both lanes of the YMM register.
     // That way when we invoke `_mm512_shuffle_epi8` we can use the same mask for both lanes.
     sz_u512_vec_t filter_even_vec, filter_odd_vec;
-    __m256i filter_ymm = _mm256_loadu_si256((__m256i const *)filter);
+    __m256i filter_ymm = _mm256_lddqu_si256((__m256i const *)filter);
     // There are a few way to initialize filters without having native strided loads.
     // In the cronological order of experiments:
     // - serial code initializing 128 bytes of odd and even mask
@@ -5079,6 +5750,9 @@ SZ_INTERNAL sz_ssize_t sz_alignment_score_avx512( //
 #pragma region ARM NEON
 
 #if SZ_USE_ARM_NEON
+#pragma GCC push_options
+#pragma GCC target("arch=armv8.2-a+simd")
+#pragma clang attribute push(__attribute__((target("arch=armv8.2-a+simd"))), apply_to = function)
 
 /**
  *  @brief  Helper structure to simplify work with 64-bit words.
@@ -5094,12 +5768,139 @@ typedef union sz_u128_vec_t {
     sz_u8_t u8s[16];
 } sz_u128_vec_t;
 
-SZ_INTERNAL sz_u64_t vreinterpretq_u8_u4(uint8x16_t vec) {
+SZ_INTERNAL sz_u64_t _sz_vreinterpretq_u8_u4(uint8x16_t vec) {
     // Use `vshrn` to produce a bitmask, similar to `movemask` in SSE.
     // https://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
     return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vec), 4)), 0) & 0x8888888888888888ull;
 }
 
+SZ_PUBLIC sz_ordering_t sz_order_neon(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length) {
+    //! Before optimizing this, read the "Operations Not Worth Optimizing" in Contributions Guide:
+    //! https://github.com/ashvardanian/StringZilla/blob/main/CONTRIBUTING.md#general-performance-observations
+    return sz_order_serial(a, a_length, b, b_length);
+}
+
+SZ_PUBLIC sz_bool_t sz_equal_neon(sz_cptr_t a, sz_cptr_t b, sz_size_t length) {
+    sz_u128_vec_t a_vec, b_vec;
+    for (; length >= 16; a += 16, b += 16, length -= 16) {
+        a_vec.u8x16 = vld1q_u8((sz_u8_t const *)a);
+        b_vec.u8x16 = vld1q_u8((sz_u8_t const *)b);
+        uint8x16_t cmp = vceqq_u8(a_vec.u8x16, b_vec.u8x16);
+        if (vmaxvq_u8(cmp) != 255) { return sz_false_k; } // Check if all bytes match
+    }
+
+    // Handle remaining bytes
+    if (length) return sz_equal_serial(a, b, length);
+    return sz_true_k;
+}
+
+SZ_PUBLIC void sz_copy_neon(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
+    // In most cases the `source` and the `target` are not aligned, but we should
+    // at least make sure that writes don't touch many cache lines.
+    // NEON has an instruction to load and write 64 bytes at once.
+    //
+    //    sz_size_t head_length = (64 - ((sz_size_t)target % 64)) % 64; // 63 or less.
+    //    sz_size_t tail_length = (sz_size_t)(target + length) % 64;    // 63 or less.
+    //    for (; head_length; target += 1, source += 1, head_length -= 1) *target = *source;
+    //    length -= head_length;
+    //    for (; length >= 64; target += 64, source += 64, length -= 64)
+    //        vst4q_u8((sz_u8_t *)target, vld1q_u8_x4((sz_u8_t const *)source));
+    //    for (; tail_length; target += 1, source += 1, tail_length -= 1) *target = *source;
+    //
+    // Sadly, those instructions end up being 20% slower than the code processing 16 bytes at a time:
+    for (; length >= 16; target += 16, source += 16, length -= 16)
+        vst1q_u8((sz_u8_t *)target, vld1q_u8((sz_u8_t const *)source));
+    if (length) sz_copy_serial(target, source, length);
+}
+
+SZ_PUBLIC void sz_move_neon(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
+    // When moving small buffers, using a small buffer on stack as a temporary storage is faster.
+
+    if (target < source || target >= source + length) {
+        // Non-overlapping, proceed forward
+        sz_copy_neon(target, source, length);
+    }
+    else {
+        // Overlapping, proceed backward
+        target += length;
+        source += length;
+
+        sz_u128_vec_t src_vec;
+        while (length >= 16) {
+            target -= 16, source -= 16, length -= 16;
+            src_vec.u8x16 = vld1q_u8((sz_u8_t const *)source);
+            vst1q_u8((sz_u8_t *)target, src_vec.u8x16);
+        }
+        while (length) {
+            target -= 1, source -= 1, length -= 1;
+            *target = *source;
+        }
+    }
+}
+
+SZ_PUBLIC void sz_fill_neon(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
+    uint8x16_t fill_vec = vdupq_n_u8(value); // Broadcast the value across the register
+
+    while (length >= 16) {
+        vst1q_u8((sz_u8_t *)target, fill_vec);
+        target += 16;
+        length -= 16;
+    }
+
+    // Handle remaining bytes
+    if (length) sz_fill_serial(target, length, value);
+}
+
+SZ_PUBLIC void sz_look_up_transform_neon(sz_cptr_t source, sz_size_t length, sz_cptr_t lut, sz_ptr_t target) {
+
+    // If the input is tiny (especially smaller than the look-up table itself), we may end up paying
+    // more for organizing the SIMD registers and changing the CPU state, than for the actual computation.
+    if (length <= 128) {
+        sz_look_up_transform_serial(source, length, lut, target);
+        return;
+    }
+
+    sz_size_t head_length = (16 - ((sz_size_t)target % 16)) % 16; // 15 or less.
+    sz_size_t tail_length = (sz_size_t)(target + length) % 16;    // 15 or less.
+
+    // We need to pull the lookup table into 16x NEON registers. We have a total of 32 such registers.
+    // According to the Neoverse V2 manual, the 4-table lookup has a latency of 6 cycles, and 4x throughput.
+    uint8x16x4_t lut_0_to_63_vec, lut_64_to_127_vec, lut_128_to_191_vec, lut_192_to_255_vec;
+    lut_0_to_63_vec = vld1q_u8_x4((sz_u8_t const *)(lut + 0));
+    lut_64_to_127_vec = vld1q_u8_x4((sz_u8_t const *)(lut + 64));
+    lut_128_to_191_vec = vld1q_u8_x4((sz_u8_t const *)(lut + 128));
+    lut_192_to_255_vec = vld1q_u8_x4((sz_u8_t const *)(lut + 192));
+
+    sz_u128_vec_t source_vec;
+    // If the top bit is set in each word of `source_vec`, than we use `lookup_128_to_191_vec` or
+    // `lookup_192_to_255_vec`. If the second bit is set, we use `lookup_64_to_127_vec` or `lookup_192_to_255_vec`.
+    sz_u128_vec_t lookup_0_to_63_vec, lookup_64_to_127_vec, lookup_128_to_191_vec, lookup_192_to_255_vec;
+    sz_u128_vec_t blended_0_to_255_vec;
+
+    // Process the head with serial code
+    for (; head_length; target += 1, source += 1, head_length -= 1) *target = lut[*(sz_u8_t const *)source];
+
+    // Table lookups on Arm are much simpler to use than on x86, as we can use the `vqtbl4q_u8` instruction
+    // to perform a 4-table lookup in a single instruction. The XORs are used to adjust the lookup position
+    // within each 64-byte range of the table.
+    // Details on the 4-table lookup: https://lemire.me/blog/2019/07/23/arbitrary-byte-to-byte-maps-using-arm-neon/
+    length -= head_length;
+    length -= tail_length;
+    for (; length >= 16; source += 16, target += 16, length -= 16) {
+        source_vec.u8x16 = vld1q_u8((sz_u8_t const *)source);
+        lookup_0_to_63_vec.u8x16 = vqtbl4q_u8(lut_0_to_63_vec, source_vec.u8x16);
+        lookup_64_to_127_vec.u8x16 = vqtbl4q_u8(lut_64_to_127_vec, veorq_u8(source_vec.u8x16, vdupq_n_u8(0x40)));
+        lookup_128_to_191_vec.u8x16 = vqtbl4q_u8(lut_128_to_191_vec, veorq_u8(source_vec.u8x16, vdupq_n_u8(0x80)));
+        lookup_192_to_255_vec.u8x16 = vqtbl4q_u8(lut_192_to_255_vec, veorq_u8(source_vec.u8x16, vdupq_n_u8(0xc0)));
+        blended_0_to_255_vec.u8x16 = vorrq_u8(vorrq_u8(lookup_0_to_63_vec.u8x16, lookup_64_to_127_vec.u8x16),
+                                              vorrq_u8(lookup_128_to_191_vec.u8x16, lookup_192_to_255_vec.u8x16));
+        vst1q_u8((sz_u8_t *)target, blended_0_to_255_vec.u8x16);
+    }
+
+    // Process the tail with serial code
+    for (; tail_length; target += 1, source += 1, tail_length -= 1) *target = lut[*(sz_u8_t const *)source];
+}
+
 SZ_PUBLIC sz_cptr_t sz_find_byte_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n) {
     sz_u64_t matches;
     sz_u128_vec_t h_vec, n_vec, matches_vec;
@@ -5111,7 +5912,7 @@ SZ_PUBLIC sz_cptr_t sz_find_byte_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t
         // In Arm NEON we don't have a `movemask` to combine it with `ctz` and get the offset of the match.
         // But assuming the `vmaxvq` is cheap, we can use it to find the first match, by blending (bitwise selecting)
         // the vector with a relative offsets array.
-        matches = vreinterpretq_u8_u4(matches_vec.u8x16);
+        matches = _sz_vreinterpretq_u8_u4(matches_vec.u8x16);
         if (matches) return h + sz_u64_ctz(matches) / 4;
 
         h += 16, h_length -= 16;
@@ -5128,7 +5929,7 @@ SZ_PUBLIC sz_cptr_t sz_rfind_byte_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_
     while (h_length >= 16) {
         h_vec.u8x16 = vld1q_u8((sz_u8_t const *)h + h_length - 16);
         matches_vec.u8x16 = vceqq_u8(h_vec.u8x16, n_vec.u8x16);
-        matches = vreinterpretq_u8_u4(matches_vec.u8x16);
+        matches = _sz_vreinterpretq_u8_u4(matches_vec.u8x16);
         if (matches) return h + h_length - 1 - sz_u64_clz(matches) / 4;
         h_length -= 16;
     }
@@ -5153,7 +5954,7 @@ SZ_PUBLIC sz_u64_t _sz_find_charset_neon_register(sz_u128_vec_t h_vec, uint8x16_
     uint8x16_t matches_vec = vorrq_u8(matches_top_vec, matches_bottom_vec);
     // Istead of pure `vandq_u8`, we can immediately broadcast a match presence across each 8-bit word.
     matches_vec = vtstq_u8(matches_vec, byte_mask_vec);
-    return vreinterpretq_u8_u4(matches_vec);
+    return _sz_vreinterpretq_u8_u4(matches_vec);
 }
 
 SZ_PUBLIC sz_cptr_t sz_find_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n, sz_size_t n_length) {
@@ -5178,7 +5979,7 @@ SZ_PUBLIC sz_cptr_t sz_find_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n, s
             h_last_vec.u8x16 = vld1q_u8((sz_u8_t const *)(h + 1));
             matches_vec.u8x16 =
                 vandq_u8(vceqq_u8(h_first_vec.u8x16, n_first_vec.u8x16), vceqq_u8(h_last_vec.u8x16, n_last_vec.u8x16));
-            matches = vreinterpretq_u8_u4(matches_vec.u8x16);
+            matches = _sz_vreinterpretq_u8_u4(matches_vec.u8x16);
             if (matches) return h + sz_u64_ctz(matches) / 4;
         }
     }
@@ -5200,7 +6001,7 @@ SZ_PUBLIC sz_cptr_t sz_find_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n, s
                     vceqq_u8(h_first_vec.u8x16, n_first_vec.u8x16), //
                     vceqq_u8(h_mid_vec.u8x16, n_mid_vec.u8x16)),
                 vceqq_u8(h_last_vec.u8x16, n_last_vec.u8x16));
-            matches = vreinterpretq_u8_u4(matches_vec.u8x16);
+            matches = _sz_vreinterpretq_u8_u4(matches_vec.u8x16);
             if (matches) return h + sz_u64_ctz(matches) / 4;
         }
     }
@@ -5224,7 +6025,7 @@ SZ_PUBLIC sz_cptr_t sz_find_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n, s
                     vceqq_u8(h_first_vec.u8x16, n_first_vec.u8x16), //
                     vceqq_u8(h_mid_vec.u8x16, n_mid_vec.u8x16)),
                 vceqq_u8(h_last_vec.u8x16, n_last_vec.u8x16));
-            matches = vreinterpretq_u8_u4(matches_vec.u8x16);
+            matches = _sz_vreinterpretq_u8_u4(matches_vec.u8x16);
             while (matches) {
                 int potential_offset = sz_u64_ctz(matches) / 4;
                 if (sz_equal(h + potential_offset, n, n_length)) return h + potential_offset;
@@ -5264,7 +6065,7 @@ SZ_PUBLIC sz_cptr_t sz_rfind_neon(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n,
                 vceqq_u8(h_first_vec.u8x16, n_first_vec.u8x16), //
                 vceqq_u8(h_mid_vec.u8x16, n_mid_vec.u8x16)),
             vceqq_u8(h_last_vec.u8x16, n_last_vec.u8x16));
-        matches = vreinterpretq_u8_u4(matches_vec.u8x16);
+        matches = _sz_vreinterpretq_u8_u4(matches_vec.u8x16);
         while (matches) {
             int potential_offset = sz_u64_clz(matches) / 4;
             if (sz_equal(h + h_length - n_length - potential_offset, n, n_length))
@@ -5309,10 +6110,135 @@ SZ_PUBLIC sz_cptr_t sz_rfind_charset_neon(sz_cptr_t h, sz_size_t h_length, sz_ch
     return sz_rfind_charset_serial(h, h_length, set);
 }
 
+#pragma clang attribute pop
+#pragma GCC pop_options
 #endif // Arm Neon
 
 #pragma endregion
 
+/*  @brief  Implementation of the string search algorithms using the Arm SVE variable-length registers, available
+ *          in Arm v9 processors.
+ *
+ *  Implements:
+ *      - memory: {copy, move, fill}
+ *      - comparisons: {equal, order}
+ *      - search: {substring, character, character set} x {forward, reverse}.
+ */
+#pragma region ARM SVE
+
+#if SZ_USE_ARM_SVE
+#pragma GCC push_options
+#pragma GCC target("arch=armv8.2-a+sve")
+#pragma clang attribute push(__attribute__((target("arch=armv8.2-a+sve"))), apply_to = function)
+
+SZ_PUBLIC void sz_fill_sve(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
+    svuint8_t value_vec = svdup_u8(value);
+    sz_size_t vec_len = svcntb(); // Vector length in bytes (scalable)
+
+    if (length <= vec_len) {
+        // Small buffer case: use mask to handle small writes
+        svbool_t mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)length);
+        svst1_u8(mask, (unsigned char *)target, value_vec);
+    }
+    else {
+        // Calculate head, body, and tail sizes
+        sz_size_t head_length = vec_len - ((sz_size_t)target % vec_len);
+        sz_size_t tail_length = (sz_size_t)(target + length) % vec_len;
+        sz_size_t body_length = length - head_length - tail_length;
+
+        // Handle unaligned head
+        svbool_t head_mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)head_length);
+        svst1_u8(head_mask, (unsigned char *)target, value_vec);
+        target += head_length;
+
+        // Aligned body loop
+        for (; body_length >= vec_len; target += vec_len, body_length -= vec_len) {
+            svst1_u8(svptrue_b8(), (unsigned char *)target, value_vec);
+        }
+
+        // Handle unaligned tail
+        svbool_t tail_mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)tail_length);
+        svst1_u8(tail_mask, (unsigned char *)target, value_vec);
+    }
+}
+
+SZ_PUBLIC void sz_copy_sve(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
+    sz_size_t vec_len = svcntb(); // Vector length in bytes
+
+    // Arm Neoverse V2 cores in Graviton 4, for example, come with 256 KB of L1 data cache per core,
+    // and 8 MB of L2 cache per core. Moreover, the L1 cache is fully associative.
+    // With two strings, we may consider the overal workload huge, if each exceeds 1 MB in length.
+    //
+    //      int is_huge = length >= 4ull * 1024ull * 1024ull;
+    //
+    // When the buffer is small, there isn't much to innovate.
+    if (length <= vec_len) {
+        // Small buffer case: use mask to handle small writes
+        svbool_t mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)length);
+        svuint8_t data = svld1_u8(mask, (unsigned char *)source);
+        svst1_u8(mask, (unsigned char *)target, data);
+    }
+    // When dealing with larger buffers, similar to AVX-512, we want minimize unaligned operations
+    // and handle the head, body, and tail separately. We can also traverse the buffer in both directions
+    // as Arm generally supports more simultaneous stores than x86 CPUs.
+    //
+    // For gigantic datasets, similar to AVX-512, non-temporal "loads" and "stores" can be used.
+    // Sadly, if the register size (16 byte or larger) is smaller than a cache-line (64 bytes)
+    // we will pay a huge penalty on loads, fetching the same content many times.
+    // It may be better to allow caching (and subsequent eviction), in favor of using four-element
+    // tuples, wich will be guaranteed to be a multiple of a cache line.
+    //
+    // Another approach is to use the `LD4B` instructions, which will populate four registers at once.
+    // This however, further decreases the performance from LibC-like 29 GB/s to 20 GB/s.
+    else {
+        // Calculating head, body, and tail sizes depends on the `vec_len`,
+        // but it's runtime constant, and the modulo operation is expensive!
+        // Instead we use the fact, that it's always a multiple of 128 bits or 16 bytes.
+        sz_size_t head_length = 16 - ((sz_size_t)target % 16);
+        sz_size_t tail_length = (sz_size_t)(target + length) % 16;
+        sz_size_t body_length = length - head_length - tail_length;
+
+        // Handle unaligned parts
+        svbool_t head_mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)head_length);
+        svuint8_t head_data = svld1_u8(head_mask, (unsigned char *)source);
+        svst1_u8(head_mask, (unsigned char *)target, head_data);
+        svbool_t tail_mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)tail_length);
+        svuint8_t tail_data = svld1_u8(tail_mask, (unsigned char *)source + head_length + body_length);
+        svst1_u8(tail_mask, (unsigned char *)target + head_length + body_length, tail_data);
+        target += head_length;
+        source += head_length;
+
+        // Aligned body loop, walking in two directions
+        for (; body_length >= vec_len * 2; target += vec_len, source += vec_len, body_length -= vec_len * 2) {
+            svuint8_t forward_data = svld1_u8(svptrue_b8(), (unsigned char *)source);
+            svuint8_t backward_data = svld1_u8(svptrue_b8(), (unsigned char *)source + body_length - vec_len);
+            svst1_u8(svptrue_b8(), (unsigned char *)target, forward_data);
+            svst1_u8(svptrue_b8(), (unsigned char *)target + body_length - vec_len, backward_data);
+        }
+        // Up to (vec_len * 2 - 1) bytes of data may be left in the body,
+        // so we can unroll the last two optional loop iterations.
+        if (body_length > vec_len) {
+            svbool_t mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)body_length);
+            svuint8_t data = svld1_u8(mask, (unsigned char *)source);
+            svst1_u8(mask, (unsigned char *)target, data);
+            body_length -= vec_len;
+            source += body_length;
+            target += body_length;
+        }
+        if (body_length) {
+            svbool_t mask = svwhilelt_b8((sz_u32_t)0ull, (sz_u32_t)body_length);
+            svuint8_t data = svld1_u8(mask, (unsigned char *)source);
+            svst1_u8(mask, (unsigned char *)target, data);
+        }
+    }
+}
+
+#pragma clang attribute pop
+#pragma GCC pop_options
+#endif // Arm SVE
+
+#pragma endregion
+
 /*
  *  @brief  Pick the right implementation for the string search algorithms.
  */
@@ -5346,6 +6272,10 @@ SZ_PUBLIC void sz_hashes_fingerprint(sz_cptr_t start, sz_size_t length, sz_size_
 SZ_DYNAMIC sz_bool_t sz_equal(sz_cptr_t a, sz_cptr_t b, sz_size_t length) {
 #if SZ_USE_X86_AVX512
     return sz_equal_avx512(a, b, length);
+#elif SZ_USE_X86_AVX2
+    return sz_equal_avx2(a, b, length);
+#elif SZ_USE_ARM_NEON
+    return sz_equal_neon(a, b, length);
 #else
     return sz_equal_serial(a, b, length);
 #endif
@@ -5354,6 +6284,10 @@ SZ_DYNAMIC sz_bool_t sz_equal(sz_cptr_t a, sz_cptr_t b, sz_size_t length) {
 SZ_DYNAMIC sz_ordering_t sz_order(sz_cptr_t a, sz_size_t a_length, sz_cptr_t b, sz_size_t b_length) {
 #if SZ_USE_X86_AVX512
     return sz_order_avx512(a, a_length, b, b_length);
+#elif SZ_USE_X86_AVX2
+    return sz_order_avx2(a, a_length, b, b_length);
+#elif SZ_USE_ARM_NEON
+    return sz_order_neon(a, a_length, b, b_length);
 #else
     return sz_order_serial(a, a_length, b, b_length);
 #endif
@@ -5364,6 +6298,8 @@ SZ_DYNAMIC void sz_copy(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
     sz_copy_avx512(target, source, length);
 #elif SZ_USE_X86_AVX2
     sz_copy_avx2(target, source, length);
+#elif SZ_USE_ARM_NEON
+    sz_copy_neon(target, source, length);
 #else
     sz_copy_serial(target, source, length);
 #endif
@@ -5374,6 +6310,8 @@ SZ_DYNAMIC void sz_move(sz_ptr_t target, sz_cptr_t source, sz_size_t length) {
     sz_move_avx512(target, source, length);
 #elif SZ_USE_X86_AVX2
     sz_move_avx2(target, source, length);
+#elif SZ_USE_ARM_NEON
+    sz_move_neon(target, source, length);
 #else
     sz_move_serial(target, source, length);
 #endif
@@ -5384,11 +6322,25 @@ SZ_DYNAMIC void sz_fill(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
     sz_fill_avx512(target, length, value);
 #elif SZ_USE_X86_AVX2
     sz_fill_avx2(target, length, value);
+#elif SZ_USE_ARM_NEON
+    sz_fill_neon(target, length, value);
 #else
     sz_fill_serial(target, length, value);
 #endif
 }
 
+SZ_DYNAMIC void sz_look_up_transform(sz_cptr_t source, sz_size_t length, sz_cptr_t lut, sz_ptr_t target) {
+#if SZ_USE_X86_AVX512
+    sz_look_up_transform_avx512(source, length, lut, target);
+#elif SZ_USE_X86_AVX2
+    sz_look_up_transform_avx2(source, length, lut, target);
+#elif SZ_USE_ARM_NEON
+    sz_look_up_transform_neon(source, length, lut, target);
+#else
+    sz_look_up_transform_serial(source, length, lut, target);
+#endif
+}
+
 SZ_DYNAMIC sz_cptr_t sz_find_byte(sz_cptr_t haystack, sz_size_t h_length, sz_cptr_t needle) {
 #if SZ_USE_X86_AVX512
     return sz_find_byte_avx512(haystack, h_length, needle);
@@ -5452,6 +6404,8 @@ SZ_DYNAMIC sz_cptr_t sz_find_charset(sz_cptr_t text, sz_size_t length, sz_charse
 SZ_DYNAMIC sz_cptr_t sz_rfind_charset(sz_cptr_t text, sz_size_t length, sz_charset_t const *set) {
 #if SZ_USE_X86_AVX512
     return sz_rfind_charset_avx512(text, length, set);
+#elif SZ_USE_X86_AVX2
+    return sz_rfind_charset_avx2(text, length, set);
 #elif SZ_USE_ARM_NEON
     return sz_rfind_charset_neon(text, length, set);
 #else
diff --git a/include/stringzilla/stringzilla.hpp b/include/stringzilla/stringzilla.hpp
index d7324a85..6a65038f 100644
--- a/include/stringzilla/stringzilla.hpp
+++ b/include/stringzilla/stringzilla.hpp
@@ -79,6 +79,40 @@ using string_view = basic_string_slice<char const>;
 template <std::size_t count_characters>
 using carray = char[count_characters];
 
+#pragma region Memory Operations
+
+/**
+ *  @brief  Analog to @b `std::memset`, but with a more efficient implementation.
+ *  @param  target The pointer to the target memory region.
+ *  @param  value The byte value to set.
+ *  @param  n The number of bytes to copy.
+ */
+inline void memset(void *target, char value, std::size_t n) noexcept {
+    return sz_fill(reinterpret_cast<sz_ptr_t>(target), n, value);
+}
+
+/**
+ *  @brief  Analog to @b `std::memmove`, but with a more efficient implementation.
+ *  @param  target The pointer to the target memory region.
+ *  @param  source The pointer to the source memory region.
+ *  @param  n The number of bytes to copy.
+ */
+inline void memmove(void *target, void const *source, std::size_t n) noexcept {
+    return sz_move(reinterpret_cast<sz_ptr_t>(target), reinterpret_cast<sz_cptr_t>(source), n);
+}
+
+/**
+ *  @brief  Analog to @b `std::memcpy`, but with a more efficient implementation.
+ *  @param  target The pointer to the target memory region.
+ *  @param  source The pointer to the source memory region.
+ *  @param  n The number of bytes to copy.
+ */
+inline void memcpy(void *target, void const *source, std::size_t n) noexcept {
+    return sz_copy(reinterpret_cast<sz_ptr_t>(target), reinterpret_cast<sz_cptr_t>(source), n);
+}
+
+#pragma endregion
+
 #pragma region Character Sets
 
 /**
@@ -306,6 +340,55 @@ inline char_set whitespaces_set() { return char_set {whitespaces()}; }
 inline char_set newlines_set() { return char_set {newlines()}; }
 inline char_set base64_set() { return char_set {base64()}; }
 
+/**
+ *  @brief  A look-up table for character replacement operations.
+ *          Exactly 256 bytes for byte-to-byte replacement.
+ *          ! For larger character types should be allocated on the heap.
+ */
+template <typename char_type_ = char>
+class basic_look_up_table {
+    static_assert(sizeof(char_type_) == 1 || sizeof(char_type_) == 2 || sizeof(char_type_) == 4,
+                  "Character type must be 1, 2, or 4 bytes long");
+    static constexpr std::size_t size_k = sizeof(char_type_) == 1   ? 256ul
+                                          : sizeof(char_type_) == 2 ? 65536ul
+                                                                    : 4294967296ul;
+    static constexpr std::size_t bytes_k = size_k * sizeof(char_type_);
+    using usnigned_type_ = typename std::make_unsigned<char_type_>::type;
+
+    char_type_ lut_[size_k];
+
+  public:
+    using char_type = char_type_;
+
+    basic_look_up_table() noexcept { memset(&lut_[0], 0, bytes_k); }
+    explicit basic_look_up_table(char_type const (&chars)[size_k]) noexcept { memcpy(&lut_[0], chars, bytes_k); }
+    basic_look_up_table(std::array<char_type, size_k> const &chars) noexcept {
+        memcpy(&lut_[0], chars.data(), bytes_k);
+    }
+
+    basic_look_up_table(basic_look_up_table const &other) noexcept { memcpy(&lut_[0], other.lut_, bytes_k); }
+    basic_look_up_table &operator=(basic_look_up_table const &other) noexcept {
+        memcpy(&lut_[0], other.lut_, bytes_k);
+        return *this;
+    }
+
+    /**
+     *  @brief  Creates a look-up table with a one-to-one mapping of characters to themselves.
+     *  Similar to `std::iota` filling, but properly handles signed integer casts.
+     */
+    static basic_look_up_table identity() noexcept {
+        basic_look_up_table result;
+        for (std::size_t i = 0; i < size_k; ++i) { result.lut_[i] = static_cast<usnigned_type_>(i); }
+        return result;
+    }
+
+    inline sz_cptr_t raw() const noexcept { return reinterpret_cast<sz_cptr_t>(&lut_[0]); }
+    inline char_type &operator[](char_type c) noexcept { return lut_[sz_bitcast(usnigned_type_, c)]; }
+    inline char_type const &operator[](char_type c) const noexcept { return lut_[sz_bitcast(usnigned_type_, c)]; }
+};
+
+using look_up_table = basic_look_up_table<char>;
+
 #pragma endregion
 
 #pragma region Ranges of Search Matches
@@ -1576,12 +1659,18 @@ class basic_string_slice {
     /**  @brief  Split the string into three parts, before the match, the match itself, and after it. */
     partition_type partition(string_view pattern) const noexcept { return partition_(pattern, pattern.length()); }
 
+    /**  @brief  Split the string into three parts, before the match, the match itself, and after it. */
+    partition_type partition(value_type pattern) const noexcept { return partition_(string_view(&pattern, 1), 1); }
+
     /**  @brief  Split the string into three parts, before the match, the match itself, and after it. */
     partition_type partition(char_set pattern) const noexcept { return partition_(pattern, 1); }
 
     /**  @brief  Split the string into three parts, before the @b last match, the last match itself, and after it. */
     partition_type rpartition(string_view pattern) const noexcept { return rpartition_(pattern, pattern.length()); }
 
+    /**  @brief  Split the string into three parts, before the @b last match, the last match itself, and after it. */
+    partition_type rpartition(value_type pattern) const noexcept { return rpartition_(string_view(&pattern, 1), 1); }
+
     /**  @brief  Split the string into three parts, before the @b last match, the last match itself, and after it. */
     partition_type rpartition(char_set pattern) const noexcept { return rpartition_(pattern, 1); }
 
@@ -3315,6 +3404,24 @@ class basic_string {
         return try_replace_all_<char_set>(pattern, replacement);
     }
 
+    /**
+     *  @brief  Replaces ( @b in-place ) all characters in the string using the provided lookup table.
+     */
+    basic_string &transform(look_up_table const &table) noexcept {
+        transform(table, data());
+        return *this;
+    }
+
+    /**
+     *  @brief  Maps all chatacters in the current string into another buffer using the provided lookup table.
+     */
+    void transform(look_up_table const &table, pointer output) const noexcept {
+        sz_ptr_t start;
+        sz_size_t length;
+        sz_string_range(&string_, &start, &length);
+        sz_look_up_transform((sz_cptr_t)start, (sz_size_t)length, (sz_cptr_t)table.raw(), (sz_ptr_t)output);
+    }
+
   private:
     template <typename pattern_type>
     bool try_replace_all_(pattern_type pattern, string_view replacement) noexcept;
@@ -3757,6 +3864,26 @@ void randomize(basic_string_slice<char_type_> string, generator_type_ &generator
     sz_generate(alphabet.data(), alphabet.size(), string.data(), string.size(), generator_callback, &generator);
 }
 
+/**
+ *  @brief  Replaces ( @b in-place ) all characters in the string using the provided lookup table.
+ */
+template <typename char_type_>
+void transform(basic_string_slice<char_type_> string, basic_look_up_table<char_type_> const &table) noexcept {
+    static_assert(sizeof(char_type_) == 1, "The character type must be 1 byte long.");
+    sz_look_up_transform((sz_cptr_t)string.data(), (sz_size_t)string.size(), (sz_cptr_t)table.raw(),
+                         (sz_ptr_t)string.data());
+}
+
+/**
+ *  @brief  Maps all chatacters in the current string into another buffer using the provided lookup table.
+ */
+template <typename char_type_>
+void transform(basic_string_slice<char_type_ const> source, basic_look_up_table<char_type_> const &table,
+               char_type_ *target) noexcept {
+    static_assert(sizeof(char_type_) == 1, "The character type must be 1 byte long.");
+    sz_look_up_transform((sz_cptr_t)source.data(), (sz_size_t)source.size(), (sz_cptr_t)table.raw(), (sz_ptr_t)target);
+}
+
 /**
  *  @brief  Overwrites the string slice with random characters from the given alphabet
  *          using `std::rand` as the random generator.
diff --git a/pyproject.toml b/pyproject.toml
index 8fcdcfb9..38bca547 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -79,40 +79,43 @@ before-build = ["rd /s /q {project}\\build || echo Done"]
 [[tool.cibuildwheel.overrides]]
 select = "*-win_amd64"
 inherit.environment = "append"
-environment.SZ_X86_64="1"
+environment.SZ_X86_64 = "1"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-manylinux*_x86_64"
 inherit.environment = "append"
-environment.SZ_X86_64="1"
+environment.SZ_X86_64 = "1"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-musllinux*_x86_64"
 inherit.environment = "append"
-environment.SZ_X86_64="1"
+environment.SZ_X86_64 = "1"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-macos*_x86_64"
 inherit.environment = "append"
-environment.SZ_X86_64="1"
+environment.SZ_X86_64 = "1"
 
 # Detect ARM 64-bit builds
 [[tool.cibuildwheel.overrides]]
 select = "*-win_arm64"
 inherit.environment = "append"
-environment.SZ_ARM64="1"
+environment.SZ_ARM64 = "1"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-manylinux*_aarch64"
 inherit.environment = "append"
-environment.SZ_ARM64="1"
+environment.SZ_ARM64 = "1"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-musllinux*_aarch64"
 inherit.environment = "append"
-environment.SZ_ARM64="1"
+environment.SZ_ARM64 = "1"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-macos*_arm64"
 inherit.environment = "append"
-environment.SZ_ARM64="1"
+environment.SZ_ARM64 = "1"
+
+[tool.cibuildwheel.macos.environment]
+MACOSX_DEPLOYMENT_TARGET = "10.11"
diff --git a/python/lib.c b/python/lib.c
index 75093a87..85aef1aa 100644
--- a/python/lib.c
+++ b/python/lib.c
@@ -1927,6 +1927,53 @@ static PyObject *Str_endswith(PyObject *self, PyObject *args, PyObject *kwargs)
     else { Py_RETURN_FALSE; }
 }
 
+static PyObject *Str_translate(PyObject *self, PyObject *args, PyObject *kwargs) {
+    int is_member = self != NULL && PyObject_TypeCheck(self, &StrType);
+    Py_ssize_t nargs = PyTuple_Size(args);
+    if (nargs < !is_member + 1 || nargs > !is_member + 3) {
+        PyErr_Format(PyExc_TypeError, "Invalid number of arguments");
+        return NULL;
+    }
+
+    PyObject *str_obj = is_member ? self : PyTuple_GET_ITEM(args, 0);
+    PyObject *look_up_table_obj = PyTuple_GET_ITEM(args, !is_member);
+    PyObject *start_obj = nargs > !is_member + 1 ? PyTuple_GET_ITEM(args, !is_member + 1) : NULL;
+    PyObject *end_obj = nargs > !is_member + 2 ? PyTuple_GET_ITEM(args, !is_member + 2) : NULL;
+
+    // Optional start and end arguments
+    Py_ssize_t start = 0, end = PY_SSIZE_T_MAX;
+
+    if (start_obj && ((start = PyLong_AsSsize_t(start_obj)) == -1 && PyErr_Occurred())) {
+        PyErr_SetString(PyExc_TypeError, "start must be an integer");
+        return NULL;
+    }
+
+    if (end_obj && ((end = PyLong_AsSsize_t(end_obj)) == -1 && PyErr_Occurred())) {
+        PyErr_SetString(PyExc_TypeError, "end must be an integer");
+        return NULL;
+    }
+
+    sz_string_view_t str, look_up_table;
+    if (!export_string_like(str_obj, &str.start, &str.length) ||
+        !export_string_like(look_up_table_obj, &look_up_table.start, &look_up_table.length)) {
+        PyErr_SetString(PyExc_TypeError, "Both arguments must be string-like");
+        return NULL;
+    }
+
+    // Apply start and end arguments
+    str.start += start;
+    str.length -= start;
+    if (end != PY_SSIZE_T_MAX && end - start < str.length) { str.length = end - start; }
+
+    if (look_up_table.length != 256) {
+        PyErr_SetString(PyExc_ValueError, "The look-up table must be exactly 256 bytes long");
+        return NULL;
+    }
+
+    sz_look_up_transform(str.start, str.length, look_up_table.start, str.start);
+    return Py_None;
+}
+
 static PyObject *Str_find_first_of(PyObject *self, PyObject *args, PyObject *kwargs) {
     Py_ssize_t signed_offset;
     sz_string_view_t text;
@@ -2438,6 +2485,7 @@ static PyMethodDef Str_methods[] = {
     {"splitlines", Str_splitlines, SZ_METHOD_FLAGS, "Split a string by line breaks."},
     {"startswith", Str_startswith, SZ_METHOD_FLAGS, "Check if a string starts with a given prefix."},
     {"endswith", Str_endswith, SZ_METHOD_FLAGS, "Check if a string ends with a given suffix."},
+    {"translate", Str_translate, SZ_METHOD_FLAGS, "Look-Up Table in-place transformation of a byte-string."},
     {"decode", Str_decode, SZ_METHOD_FLAGS, "Decode the bytes into `str` with a given encoding"},
 
     // Bidirectional operations
@@ -3139,6 +3187,7 @@ static PyMethodDef stringzilla_methods[] = {
     {"splitlines", Str_splitlines, SZ_METHOD_FLAGS, "Split a string by line breaks."},
     {"startswith", Str_startswith, SZ_METHOD_FLAGS, "Check if a string starts with a given prefix."},
     {"endswith", Str_endswith, SZ_METHOD_FLAGS, "Check if a string ends with a given suffix."},
+    {"translate", Str_translate, SZ_METHOD_FLAGS, "Look-Up Table in-place transformation of a byte-string."},
     {"decode", Str_decode, SZ_METHOD_FLAGS, "Decode the bytes into `str` with a given encoding"},
 
     // Bidirectional operations
diff --git a/scripts/bench.hpp b/scripts/bench.hpp
index 03bc4d1e..ecdf3bb2 100644
--- a/scripts/bench.hpp
+++ b/scripts/bench.hpp
@@ -27,6 +27,11 @@ namespace scripts {
 
 using seconds_t = double;
 
+template <std::size_t multiple>
+std::size_t round_up_to_multiple(std::size_t n) {
+    return n == 0 ? multiple : ((n + multiple - 1) / multiple) * multiple;
+}
+
 struct benchmark_result_t {
     std::size_t iterations = 0;
     std::size_t bytes_passed = 0;
@@ -156,7 +161,7 @@ inline std::vector<result_string_type> filter_by_length(std::vector<from_string_
     return result;
 }
 
-inline static std::size_t seconds_per_benchmark = 5;
+inline static std::size_t seconds_per_benchmark = SZ_DEBUG ? 1 : 5;
 
 struct dataset_t {
     std::string text;
diff --git a/scripts/bench_memory.cpp b/scripts/bench_memory.cpp
new file mode 100644
index 00000000..eb72fc96
--- /dev/null
+++ b/scripts/bench_memory.cpp
@@ -0,0 +1,278 @@
+/**
+ *  @file   bench_memory.cpp
+ *  @brief  Benchmarks for memory operations like copying, moving, and comparing.
+ *
+ *  This file is the sibling of `bench_sort.cpp`, `bench_token.cpp` and `bench_similarity.cpp`.
+ *  It accepts a file with a list of words, and benchmarks the memory operations on them.
+ */
+#include <cstring> // `memmem`
+#include <memory>  // `std::unique_ptr`
+#include <numeric> // `std::iota`
+#include <string>  // `std::string`
+
+#ifdef _WIN32
+#include <malloc.h> // `_aligned_malloc`
+#else
+#include <cstdlib> // `std::aligned_alloc`
+#endif
+
+#define SZ_USE_MISALIGNED_LOADS (1)
+#include <bench.hpp>
+
+using namespace ashvardanian::stringzilla::scripts;
+constexpr std::size_t max_shift_length = 299;
+
+/**
+ *  @brief  Wraps platform-specific @b aligned memory allocation and deallocation functions.
+ *          Compatible with `std::unique_ptr` as the second template argument, to free the memory.
+ */
+struct page_alloc_and_free_t {
+#ifdef _WIN32
+    inline char *operator()(std::size_t alignment, std::size_t size) const noexcept {
+        return reinterpret_cast<char *>(_aligned_malloc(size, alignment));
+    }
+    inline void operator()(char *ptr) const noexcept { _aligned_free(ptr); }
+#else
+    inline char *operator()(std::size_t alignment, std::size_t size) const noexcept {
+        return reinterpret_cast<char *>(std::aligned_alloc(alignment, size));
+    }
+    inline void operator()(char *ptr) const noexcept { std::free(ptr); }
+#endif
+};
+
+/**
+ *  @brief  Benchmarks `memcpy`-like operations in 2 modes: aligned @b output buffer and unaligned.
+ *
+ *  In the aligned case we copy a random part of the input string into the start of a matching cache line in the output.
+ *  In the unaligned case we also locate a matching cache line in the output, but shift by one to guarantee unaligned
+ *  writes.
+ *
+ *  Multiple calls to the provided functions even with the same arguments won't change the input or output.
+ *  So the kernels can be compared against the baseline `memcpy` function.
+ *
+ *  @param  output_buffer_ptr Aligned output buffer.
+ */
+template <bool aligned_output>
+tracked_unary_functions_t copy_functions(sz_cptr_t dataset_start_ptr, sz_ptr_t output_buffer_ptr) {
+    std::string suffix = aligned_output ? "<aligned>" : "<unaligned>";
+    auto wrap_sz = [dataset_start_ptr, output_buffer_ptr](auto function) -> unary_function_t {
+        return unary_function_t([function, dataset_start_ptr, output_buffer_ptr](std::string_view slice) {
+            std::size_t output_offset = slice.data() - dataset_start_ptr;
+            // Round down to the nearest multiple of a cache line width for aligned writes
+            output_offset = round_up_to_multiple<SZ_CACHE_LINE_WIDTH>(output_offset) - SZ_CACHE_LINE_WIDTH;
+            // Ensure unaligned exports if needed
+            if constexpr (!aligned_output) output_offset += 1;
+            function(output_buffer_ptr + output_offset, slice.data(), slice.size());
+            return slice.size();
+        });
+    };
+    tracked_unary_functions_t result = {
+        {"memcpy" + suffix, wrap_sz(memcpy)},
+        {"sz_copy_serial" + suffix, wrap_sz(sz_copy_serial)},
+#if SZ_USE_X86_AVX512
+        {"sz_copy_avx512" + suffix, wrap_sz(sz_copy_avx512)},
+#endif
+#if SZ_USE_X86_AVX2
+        {"sz_copy_avx2" + suffix, wrap_sz(sz_copy_avx2)},
+#endif
+#if SZ_USE_ARM_SVE
+        {"sz_copy_sve" + suffix, wrap_sz(sz_copy_sve)},
+#endif
+#if SZ_USE_ARM_NEON
+        {"sz_copy_neon" + suffix, wrap_sz(sz_copy_neon)},
+#endif
+    };
+    return result;
+}
+
+/**
+ *  @brief  Benchmarks `memset`-like operations overwriting regions of output memory filling
+ *          them with the first byte of the input regions.
+ *
+ *  Multiple calls to the provided functions even with the same arguments won't change the input or output.
+ *  So the kernels can be compared against the baseline `memset` function.
+ *
+ *  @param  output_buffer_ptr Aligned output buffer.
+ */
+tracked_unary_functions_t fill_functions(sz_cptr_t dataset_start_ptr, sz_ptr_t output_buffer_ptr) {
+    auto wrap_sz = [dataset_start_ptr, output_buffer_ptr](auto function) -> unary_function_t {
+        return unary_function_t([function, dataset_start_ptr, output_buffer_ptr](std::string_view slice) {
+            std::size_t output_offset = (std::size_t)(slice.data() - dataset_start_ptr);
+            function(output_buffer_ptr + output_offset, slice.size(), slice.front());
+            return slice.size();
+        });
+    };
+    tracked_unary_functions_t result = {
+        {"memset", unary_function_t([dataset_start_ptr, output_buffer_ptr](std::string_view slice) {
+             std::size_t output_offset = (std::size_t)(slice.data() - dataset_start_ptr);
+             memset(output_buffer_ptr + output_offset, slice.front(), slice.size());
+             return slice.size();
+         })},
+        {"sz_fill_serial", wrap_sz(sz_fill_serial)},
+#if SZ_USE_X86_AVX512
+        {"sz_fill_avx512", wrap_sz(sz_fill_avx512)},
+#endif
+#if SZ_USE_X86_AVX2
+        {"sz_fill_avx2", wrap_sz(sz_fill_avx2)},
+#endif
+#if SZ_USE_ARM_SVE
+        {"sz_fill_sve", wrap_sz(sz_fill_sve)},
+#endif
+#if SZ_USE_ARM_NEON
+        {"sz_fill_neon", wrap_sz(sz_fill_neon)},
+#endif
+    };
+    return result;
+}
+
+/**
+ *  @brief  Benchmarks `memmove`-like operations shuffling back and forth the regions of output memory.
+ *
+ *  Multiple calls to the provided functions even with the same arguments won't change the input or output.
+ *  This is achieved by performing a combination of a forward and a backward move.
+ *  So the kernels can be compared against the baseline `memmove` function.
+ *
+ *  @param  output_buffer_ptr Aligned output buffer, that ahs at least `shift` bytes of space at the end.
+ */
+tracked_unary_functions_t move_functions(sz_cptr_t dataset_start_ptr, sz_ptr_t output_buffer_ptr, std::size_t shift) {
+    std::string suffix = "<shift" + std::to_string(shift) + ">";
+    auto wrap_sz = [dataset_start_ptr, output_buffer_ptr, shift](auto function) -> unary_function_t {
+        return unary_function_t([function, dataset_start_ptr, output_buffer_ptr, shift](std::string_view slice) {
+            std::size_t output_offset = slice.data() - dataset_start_ptr;
+            // Shift forward
+            function(output_buffer_ptr + output_offset + shift, output_buffer_ptr + output_offset, slice.size());
+            // Shift backward to revert the changes
+            function(output_buffer_ptr + output_offset, output_buffer_ptr + output_offset + shift, slice.size());
+            return slice.size() * 2;
+        });
+    };
+    tracked_unary_functions_t result = {
+        {"memmove" + suffix, wrap_sz(memmove)},
+        {"sz_move_serial" + suffix, wrap_sz(sz_move_serial)},
+#if SZ_USE_X86_AVX512
+        {"sz_move_avx512" + suffix, wrap_sz(sz_move_avx512)},
+#endif
+#if SZ_USE_X86_AVX2
+        {"sz_move_avx2" + suffix, wrap_sz(sz_move_avx2)},
+#endif
+#if SZ_USE_ARM_NEON
+        {"sz_move_neon" + suffix, wrap_sz(sz_move_neon)},
+#endif
+    };
+    return result;
+}
+
+/**
+ *  @brief  Benchmarks look-up transformations on the provided slices, updating them inplace.
+ *
+ *  Performs a simple cyclical rotation of the alphabet, to test the performance of the different
+ * "look-up table"-based transformations.
+ */
+tracked_unary_functions_t transform_functions() {
+    static unsigned char look_up_table[256];
+    std::iota(std::begin(look_up_table), std::end(look_up_table), 0);
+    std::rotate(std::begin(look_up_table), std::begin(look_up_table) + 1, std::end(look_up_table));
+
+    auto wrap_sz = [](auto function) -> unary_function_t {
+        return unary_function_t([function](std::string_view slice) {
+            char *output = const_cast<char *>(slice.data());
+            function((sz_cptr_t)output, (sz_size_t)slice.size(), (sz_cptr_t)look_up_table, (sz_ptr_t)output);
+            return slice.size();
+        });
+    };
+    tracked_unary_functions_t result = {
+        {"str::transform<lookup>", unary_function_t([](std::string_view slice) {
+             char *output = const_cast<char *>(slice.data());
+             std::transform(slice.begin(), slice.end(), output, [](char c) { return look_up_table[(unsigned char)c]; });
+             return slice.size();
+         })},
+        {"str::transform<increment>", unary_function_t([](std::string_view slice) {
+             char *output = const_cast<char *>(slice.data());
+             std::transform(slice.begin(), slice.end(), output, [](char c) { return c + 1; });
+             return slice.size();
+         })},
+        {"sz_look_up_transform_serial", wrap_sz(sz_look_up_transform_serial)},
+#if SZ_USE_X86_AVX512
+        {"sz_look_up_transform_avx512", wrap_sz(sz_look_up_transform_avx512)},
+#endif
+#if SZ_USE_X86_AVX2
+        {"sz_look_up_transform_avx2", wrap_sz(sz_look_up_transform_avx2)},
+#endif
+#if SZ_USE_ARM_NEON
+        {"sz_look_up_transform_neon", wrap_sz(sz_look_up_transform_neon)},
+#endif
+    };
+    return result;
+}
+
+void bench_memory(std::vector<std::string_view> const &slices, tracked_unary_functions_t &&variants) {
+
+    for (std::size_t variant_idx = 0; variant_idx != variants.size(); ++variant_idx) {
+        auto &variant = variants[variant_idx];
+
+        // Tests
+        if (variant.function && variant.needs_testing) {
+            std::fprintf(stderr, "Testing is not currently implemented.\n");
+            exit(1);
+        }
+
+        // Benchmarks
+        if (variant.function) variant.results = bench_on_tokens(slices, variant.function);
+        variant.print();
+    }
+}
+
+void bench_memory(std::vector<std::string_view> const &slices, sz_cptr_t dataset_start_ptr,
+                  sz_ptr_t output_buffer_ptr) {
+
+    if (slices.size() == 0) return;
+    (void)dataset_start_ptr;
+    (void)output_buffer_ptr;
+
+    bench_memory(slices, copy_functions<true>(dataset_start_ptr, output_buffer_ptr));
+    bench_memory(slices, copy_functions<false>(dataset_start_ptr, output_buffer_ptr));
+    bench_memory(slices, fill_functions(dataset_start_ptr, output_buffer_ptr));
+    // bench_memory(slices, move_functions(dataset_start_ptr, output_buffer_ptr, 1));
+    // bench_memory(slices, move_functions(dataset_start_ptr, output_buffer_ptr, 8));
+    // bench_memory(slices, move_functions(dataset_start_ptr, output_buffer_ptr, SZ_CACHE_LINE_WIDTH));
+    // bench_memory(slices, move_functions(dataset_start_ptr, output_buffer_ptr, max_shift_length));
+    // bench_memory(slices, transform_functions());
+}
+
+int main(int argc, char const **argv) {
+    std::printf("StringZilla. Starting memory benchmarks.\n");
+
+    dataset_t dataset = prepare_benchmark_environment(argc, argv);
+    sz_cptr_t const dataset_start_ptr = dataset.text.data();
+
+    // These benchmarks should be heavier than substring search and other less critical operations.
+    if (!SZ_DEBUG) seconds_per_benchmark *= 5;
+
+    // Create an aligned buffer for the output
+    std::unique_ptr<char, page_alloc_and_free_t> output_buffer;
+    // Add space for at least one cache line to simplify unaligned exports
+    std::size_t const output_length = round_up_to_multiple<4096>(dataset.text.size() + max_shift_length);
+    output_buffer.reset(page_alloc_and_free_t {}(4096, output_length));
+    if (!output_buffer) {
+        std::fprintf(stderr, "Failed to allocate an output buffer of %zu bytes.\n", output_length);
+        return 1;
+    }
+    std::memcpy(output_buffer.get(), dataset.text.data(), dataset.text.size());
+
+    // Baseline benchmarks for present tokens, coming in all lengths
+    std::printf("Benchmarking on entire dataset:\n");
+    bench_memory({dataset.text}, dataset_start_ptr, output_buffer.get());
+    std::printf("Benchmarking on lines:\n");
+    bench_memory(dataset.lines, dataset_start_ptr, output_buffer.get());
+    std::printf("Benchmarking on tokens:\n");
+    bench_memory(dataset.tokens, dataset_start_ptr, output_buffer.get());
+
+    // Run benchmarks on tokens of different length
+    for (std::size_t token_length : {1, 2, 3, 4, 5, 6, 7, 8, 16, 32}) {
+        std::printf("Benchmarking on tokens of length %zu:\n", token_length);
+        bench_memory(filter_by_length<std::string_view>(dataset.tokens, token_length), dataset_start_ptr,
+                     output_buffer.get());
+    }
+    std::printf("All benchmarks passed.\n");
+    return 0;
+}
\ No newline at end of file
diff --git a/scripts/bench_token.cpp b/scripts/bench_token.cpp
index e1f9d15e..d1fffba2 100644
--- a/scripts/bench_token.cpp
+++ b/scripts/bench_token.cpp
@@ -7,8 +7,6 @@
 #include <bench.hpp>
 #include <test.hpp> // `random_string`
 
-#include <stringzilla/drafts.h> // `sz_hashes_neon`
-
 using namespace ashvardanian::stringzilla::scripts;
 
 tracked_unary_functions_t hashing_functions() {
@@ -37,11 +35,6 @@ tracked_unary_functions_t sliding_hashing_functions(std::size_t window_width, st
 #endif
 #if SZ_USE_X86_AVX2
         {"sz_hashes_avx2:" + suffix, wrap_sz(sz_hashes_avx2)},
-#endif
-#if SZ_USE_ARM_NEON
-        {"sz_hashes_neon_naive:" + suffix, wrap_sz(sz_hashes_neon_naive)},
-        {"sz_hashes_neon_readahead:" + suffix, wrap_sz(sz_hashes_neon_readahead)},
-        {"sz_hashes_neon_reusing_loads:" + suffix, wrap_sz(sz_hashes_neon_reusing_loads)},
 #endif
         {"sz_hashes_serial:" + suffix, wrap_sz(sz_hashes_serial)},
     };
@@ -99,6 +92,9 @@ tracked_binary_functions_t equality_functions() {
     tracked_binary_functions_t result = {
         {"std::string_view.==", [](std::string_view a, std::string_view b) { return (a == b); }},
         {"sz_equal_serial", wrap_sz(sz_equal_serial), true},
+#if SZ_USE_X86_AVX2
+        {"sz_equal_avx2", wrap_sz(sz_equal_avx2), true},
+#endif
 #if SZ_USE_X86_AVX512
         {"sz_equal_avx512", wrap_sz(sz_equal_avx512), true},
 #endif
@@ -123,6 +119,12 @@ tracked_binary_functions_t ordering_functions() {
              return (order == 0 ? sz_equal_k : (order < 0 ? sz_less_k : sz_greater_k));
          }},
         {"sz_order_serial", wrap_sz(sz_order_serial), true},
+#if SZ_USE_X86_AVX2
+        {"sz_order_avx2", wrap_sz(sz_order_avx2), true},
+#endif
+#if SZ_USE_X86_AVX512
+        {"sz_order_avx512", wrap_sz(sz_order_avx512), true},
+#endif
         {"memcmp",
          [](std::string_view a, std::string_view b) {
              auto order = memcmp(a.data(), b.data(), a.size() < b.size() ? a.size() : b.size());
@@ -186,6 +188,10 @@ void bench_on_input_data(int argc, char const **argv) {
     bench_unary_functions<std::vector<std::string_view>>({dataset.text}, fingerprinting_functions(128, 1024 * 1024));
 
     // Baseline benchmarks for real words, coming in all lengths
+    std::printf("Benchmarking on entire dataset:\n");
+    bench<std::vector<std::string_view>>({dataset.text});
+    std::printf("Benchmarking on real lines:\n");
+    bench(dataset.lines);
     std::printf("Benchmarking on real words:\n");
     bench(dataset.tokens);
 
diff --git a/setup.py b/setup.py
index 5573a59a..47e96e7f 100644
--- a/setup.py
+++ b/setup.py
@@ -65,6 +65,7 @@ def linux_settings() -> Tuple[List[str], List[str], List[Tuple[str]]]:
 
 
 def darwin_settings() -> Tuple[List[str], List[str], List[Tuple[str]]]:
+
     compile_args = [
         "-std=c99",  # use the C 99 language dialect
         "-pedantic",  # stick close to the C language standard, avoid compiler extensions
@@ -75,6 +76,8 @@ def darwin_settings() -> Tuple[List[str], List[str], List[Tuple[str]]]:
         "-Wno-incompatible-pointer-types",  # like: passing argument 4 of ‘sz_export_prefix_u32’ from incompatible pointer type
         "-Wno-discarded-qualifiers",  # like: passing argument 1 of ‘free’ discards ‘const’ qualifier from pointer target type
         "-fPIC",  # to enable dynamic dispatch
+        "-mfloat-abi=hard",  # NEON intrinsics not available with the soft-float ABI
+        "-mmacosx-version-min=11.0",  # minimum macOS version
     ]
     link_args = [
         "-fPIC",  # to enable dynamic dispatch


Test patch:
diff --git a/scripts/test.cpp b/scripts/test.cpp
index e09c5f59..8c1186ea 100644
--- a/scripts/test.cpp
+++ b/scripts/test.cpp
@@ -56,6 +56,7 @@ template class std::basic_string_view<char>;
 template class sz::basic_string_slice<char>;
 template class std::basic_string<char>;
 template class sz::basic_string<char>;
+template class sz::basic_charset<char>;
 
 /**
  *  @brief  Several string processing operations rely on computing integer logarithms.
@@ -126,31 +127,117 @@ static void test_arithmetical_utilities() {
                    (static_cast<sz_u8_t>(number) / static_cast<sz_u8_t>(divisor)));
 }
 
+inline void expect_equality(char const *a, char const *b, std::size_t size) {
+    if (std::memcmp(a, b, size) == 0) return;
+    std::size_t mismatch_position = 0;
+    for (; mismatch_position < size; ++mismatch_position)
+        if (a[mismatch_position] != b[mismatch_position]) break;
+    std::fprintf(stderr, "Mismatch at position %zu: %c != %c\n", mismatch_position, a[mismatch_position],
+                 b[mismatch_position]);
+    assert(false);
+}
+
 /**
- *  @brief  Validates that `sz_move` and `sz_copy` work as expected,
- *          comparing them to `std::memmove` and `std::memcpy`.
+ *  @brief  Validates that `sz::memcpy`, `sz::memset`, and `sz::memmove` work similar to their `std::` counterparts.
+ *
+ *  Uses a large heap-allocated buffer to ensure that operations optimized for @b larger-than-L2-cache memory
+ *  regions are tested. Uses a combination of deterministic and random tests with uniform and exponential distributions.
  */
-static void test_memory_utilities() {
-    constexpr std::size_t size = 1024;
-    char body_stl[size];
-    char body_sz[size];
+static void test_memory_utilities(std::size_t experiments = 1024ull * 1024ull,
+                                  std::size_t max_l2_size = 1024ull * 1024ull) {
+
+    // We will be mirroring the operations on both standard and StringZilla strings.
+    std::string text_stl(max_l2_size, '-');
+    std::string text_sz(max_l2_size, '-');
+    expect_equality(text_stl.data(), text_sz.data(), max_l2_size);
+
+    // First start with simple deterministic tests.
+    // Let's use `memset` to fill the strings with a pattern like "122333444455555...00000000000011111111111..."
+    std::size_t count_groups = 0;
+    for (std::size_t offset = 0, fill_length = 1; offset < max_l2_size;
+         offset += fill_length, ++fill_length, ++count_groups) {
+        char fill_value = '0' + fill_length % 10;
+        fill_length = offset + fill_length > max_l2_size ? max_l2_size - offset : fill_length;
+        std::memset((void *)(text_stl.data() + offset), fill_value, fill_length);
+        sz::memset((void *)(text_sz.data() + offset), fill_value, fill_length);
+        expect_equality(text_stl.data(), text_sz.data(), max_l2_size);
+    }
+
+    // Let's copy those chunks to an empty buffer one by one, validating the overall equivalency after every copy.
+    std::string copy_stl(max_l2_size, '-');
+    std::string copy_sz(max_l2_size, '-');
+    for (std::size_t offset = 0, fill_length = 1; offset < max_l2_size; offset += fill_length, ++fill_length) {
+        fill_length = offset + fill_length > max_l2_size ? max_l2_size - offset : fill_length;
+        std::memcpy((void *)(copy_stl.data() + offset), (void *)(text_stl.data() + offset), fill_length);
+        sz::memcpy((void *)(copy_sz.data() + offset), (void *)(text_sz.data() + offset), fill_length);
+        expect_equality(copy_stl.data(), copy_sz.data(), max_l2_size);
+    }
+    expect_equality(text_stl.data(), copy_stl.data(), max_l2_size);
+    expect_equality(text_sz.data(), copy_sz.data(), max_l2_size);
+
+    // Let's simulate a realistic `memmove` workloads, compacting parts of this buffer, removing all odd values,
+    // so the buffer will look like "224444666666..."
+    for (std::size_t offset = 0, fill_length = 1; offset < max_l2_size; offset += fill_length, ++fill_length) {
+        if (fill_length % 2 == 0) continue;             // Skip even chunks
+        if (offset + fill_length >= max_l2_size) break; // This is the last & there are no more even chunks to shift
+
+        // Make sure we don't overflow the buffer
+        std::size_t next_offset = offset + fill_length;
+        std::size_t next_fill_length = fill_length + 1;
+        next_fill_length = next_offset + next_fill_length > max_l2_size ? max_l2_size - next_offset : next_fill_length;
+
+        std::memmove((void *)(text_stl.data() + offset), (void *)(text_stl.data() + next_offset), next_fill_length);
+        sz::memmove((void *)(text_sz.data() + offset), (void *)(text_sz.data() + next_offset), next_fill_length);
+        expect_equality(text_stl.data(), text_sz.data(), max_l2_size);
+    }
+
+    // Now the opposite workload, expanding the buffer, inserting a dash "-" before every group of equal characters.
+    // We will need to navigate right-to left to avoid overwriting the groups.
+    std::size_t dashed_capacity = copy_stl.size() + count_groups;
+    std::size_t dashed_length = 0;
+    copy_stl.resize(dashed_capacity);
+    copy_sz.resize(dashed_capacity);
+    for (std::size_t reverse_offset = 0; reverse_offset < max_l2_size;) {
+
+        // Walk backwards to find the length of the current group
+        std::size_t offset = max_l2_size - reverse_offset - 1;
+        std::size_t fill_length = 1;
+        while (offset > 0 && copy_stl[offset - 1] == copy_stl[offset]) --offset, ++fill_length;
+
+        std::size_t new_offset = dashed_capacity - dashed_length - fill_length;
+        std::memmove((void *)(copy_stl.data() + new_offset), (void *)(copy_stl.data() + offset), fill_length);
+        sz::memmove((void *)(copy_sz.data() + new_offset), (void *)(copy_sz.data() + offset), fill_length);
+        expect_equality(copy_stl.data(), copy_sz.data(), max_l2_size);
+
+        // Put the delimiter
+        copy_stl[new_offset] = '-';
+        copy_sz[new_offset] = '-';
+        dashed_length += fill_length + 1;
+        reverse_offset += fill_length;
+    }
+
+    sz_unused(experiments);
 
+#if 0 // TODO:
+
+    // We are going to randomly select the "source" and "target" slices of the strings.
+    // For `memcpy` and `memset` the offsets should have uniform ditribution,
+    // while the length should decay with an exponential distribution.
+    // For `memmove` the offset should be uniform, but the "shift" and "length" should
+    // be exponenetial. The exponential distributions should be functions of the cache line width.
+    // https://en.cppreference.com/w/cpp/numeric/random/exponential_distribution
+    std::string dataset(max_l2_size, '-');
     auto &gen = global_random_generator();
-    uniform_uint8_distribution_t distribution;
-    std::generate(body_stl, body_stl + size, [&]() { return distribution(gen); });
-    std::copy(body_stl, body_stl + size, body_sz);
+    uniform_uint8_distribution_t alphabet_distribution('a', 'z');
+    std::uniform_int_distribution<std::size_t> length_distribution(1, max_l2_size);
+    std::exponential_distribution<double> shift_distribution(1.0 / SZ_CACHE_LINE_WIDTH);
 
     // Move the contents of both strings around, validating overall
     // equivalency after every random iteration.
-    for (std::size_t i = 0; i < size; i++) {
-        std::size_t offset = gen() % size;
-        std::size_t length = gen() % (size - offset);
-        std::size_t destination = gen() % (size - length);
-
-        std::memmove(body_stl + destination, body_stl + offset, length);
-        sz_move(body_sz + destination, body_sz + offset, length);
-        assert(std::memcmp(body_stl, body_sz, size) == 0);
+    for (std::size_t experiment = 0; experiment < experiments; experiment++) {
+        std::generate(dataset, dataset + size, [&]() { return alphabet_distribution(gen); });
     }
+#endif
 }
 
 #define assert_scoped(init, operation, condition) \
@@ -174,10 +261,10 @@ static void test_memory_utilities() {
 
 /**
  *  @brief  Invokes different C++ member methods of immutable strings to cover all STL APIs.
- *          This test guarantees API compatibility with STL `std::basic_string` template.
+ *          This test guarantees API @b compatibility with STL `std::basic_string` template.
  */
 template <typename string_type>
-static void test_api_readonly() {
+static void test_stl_compatibility_for_reads() {
 
     using str = string_type;
 
@@ -297,6 +384,17 @@ static void test_api_readonly() {
     assert(str("hello").find_first_of("hx", 0) == 0);
     assert(str("hello").find_last_of("hx", 0) == 0);
 
+    // More complex relative patterns
+    assert(str("0123456789012345678901234567890123456789012345678901234567890123") <=
+           str("0123456789012345678901234567890123456789012345678901234567890123"));
+    assert(str("0123456789012345678901234567890123456789012345678901234567890123") <=
+           str("0223456789012345678901234567890123456789012345678901234567890123"));
+    assert(str("0123456789012345678901234567890123456789012345678901234567890123") <=
+           str("0213456789012345678901234567890123456789012345678901234567890123"));
+    assert(str("12341234") <= str("12341234"));
+    assert(str("12341234") > str("12241224"));
+    assert(str("12341234") < str("13241324"));
+
     // Comparisons.
     assert(str("a") != str("b"));
     assert(str("a") < str("b"));
@@ -411,7 +509,7 @@ static void test_api_readonly() {
  *          compilation. This test guarantees API compatibility with STL `std::basic_string` template.
  */
 template <typename string_type>
-static void test_api_mutable() {
+static void test_stl_compatibility_for_updates() {
 
     using str = string_type;
 
@@ -531,7 +629,10 @@ static void test_api_mutable() {
     assert_scoped(str s = "123", (void)0, str("hello").append(s.begin(), s.end()) == "hello123");
 }
 
-static void test_stl_conversion_api() {
+/**
+ *  @brief  Constructs StringZilla classes from STL and vice-versa to ensure that the conversions are working.
+ */
+static void test_stl_conversions() {
     // From a mutable STL string to StringZilla and vice-versa.
     {
         std::string stl {"hello"};
@@ -567,7 +668,7 @@ static void test_stl_conversion_api() {
  *          extensions beyond the STL API.
  */
 template <typename string_type>
-static void test_api_readonly_extensions() {
+static void test_non_stl_extensions_for_reads() {
     using str = string_type;
 
     // Signed offset lookups and slices.
@@ -667,7 +768,7 @@ static void test_api_readonly_extensions() {
     // Computing fuzzy search results.
 }
 
-void test_api_mutable_extensions() {
+void test_non_stl_extensions_for_updates() {
     using str = sz::string;
 
     // Try methods.
@@ -718,6 +819,14 @@ void test_api_mutable_extensions() {
     assert_scoped(str s = "hello", s.replace_all(sz::char_set("x"), "xx"), s == "hello");
     assert_scoped(str s = "hello", s.replace_all(sz::char_set("lo"), "lo"), s == "helololo");
 
+    // Directly mapping bytes using a Look-Up Table.
+    sz::look_up_table invert_case = sz::look_up_table::identity();
+    for (char c = 'a'; c <= 'z'; c++) invert_case[c] = c - 'a' + 'A';
+    for (char c = 'A'; c <= 'Z'; c++) invert_case[c] = c - 'A' + 'a';
+    assert_scoped(str s = "hello", s.transform(invert_case), s == "HELLO");
+    assert_scoped(str s = "HeLLo", s.transform(invert_case), s == "hEllO");
+    assert_scoped(str s = "H-lL0", s.transform(invert_case), s == "h-Ll0");
+
     // Concatenation.
     assert(str(str("a") | str("b")) == "ab");
     assert(str(str("a") | str("b") | str("ab")) == "abab");
@@ -759,6 +868,9 @@ static void test_constructors() {
     assert(std::equal(strings.begin(), strings.end(), assignments.begin()));
 }
 
+/**
+ *  @brief  Helper structure that counts the number of allocations and deallocations.
+ */
 struct accounting_allocator : public std::allocator<char> {
     inline static bool &verbose_ref() {
         static bool global_value = false;
@@ -805,6 +917,9 @@ void assert_balanced_memory(callback_type callback) {
     assert(bytes == 0);
 }
 
+/**
+ *  @brief  Checks for memory leaks in the string class using the `accounting_allocator`.
+ */
 static void test_memory_stability_for_length(std::size_t len = 1ull << 10) {
     std::size_t iterations = 4;
 
@@ -834,7 +949,7 @@ static void test_memory_stability_for_length(std::size_t len = 1ull << 10) {
         }
     });
 
-    // How about the move ctor?
+    // How about the move constructor?
     assert_balanced_memory([&]() {
         for (std::size_t i = 0; i < iterations; i++) {
             string unique_item(base);
@@ -875,27 +990,29 @@ static void test_memory_stability_for_length(std::size_t len = 1ull << 10) {
 }
 
 /**
- *  @brief  Tests the correctness of the string class update methods, such as `append` and `erase`.
+ *  @brief  Tests the correctness of the string class update methods, such as `push_back` and `erase`.
  */
-static void test_updates() {
+static void test_updates(std::size_t repetitions = 1024) {
     // Compare STL and StringZilla strings append functionality.
     char const alphabet_chars[] = "abcdefghijklmnopqrstuvwxyz";
-    std::string stl_string;
-    sz::string sz_string;
-    for (std::size_t length = 1; length != 200; ++length) {
-        char c = alphabet_chars[std::rand() % 26];
-        stl_string.push_back(c);
-        sz_string.push_back(c);
-        assert(sz::string_view(stl_string) == sz::string_view(sz_string));
-    }
+    for (std::size_t repetition = 0; repetition != repetitions; ++repetition) {
+        std::string stl_string;
+        sz::string sz_string;
+        for (std::size_t length = 1; length != 200; ++length) {
+            char c = alphabet_chars[std::rand() % 26];
+            stl_string.push_back(c);
+            sz_string.push_back(c);
+            assert(sz::string_view(stl_string) == sz::string_view(sz_string));
+        }
 
-    // Compare STL and StringZilla strings erase functionality.
-    while (stl_string.length()) {
-        std::size_t offset_to_erase = std::rand() % stl_string.length();
-        std::size_t chars_to_erase = std::rand() % (stl_string.length() - offset_to_erase) + 1;
-        stl_string.erase(offset_to_erase, chars_to_erase);
-        sz_string.erase(offset_to_erase, chars_to_erase);
-        assert(sz::string_view(stl_string) == sz::string_view(sz_string));
+        // Compare STL and StringZilla strings erase functionality.
+        while (stl_string.length()) {
+            std::size_t offset_to_erase = std::rand() % stl_string.length();
+            std::size_t chars_to_erase = std::rand() % (stl_string.length() - offset_to_erase) + 1;
+            stl_string.erase(offset_to_erase, chars_to_erase);
+            sz_string.erase(offset_to_erase, chars_to_erase);
+            assert(sz::string_view(stl_string) == sz::string_view(sz_string));
+        }
     }
 }
 
@@ -948,12 +1065,14 @@ static void test_search() {
     assert("aabaa"_sz.strip(sz::char_set {"a"}) == "b");
 
     // Check more advanced composite operations
+    assert("abbccc"_sz.partition('b').before.size() == 1);
     assert("abbccc"_sz.partition("bb").before.size() == 1);
     assert("abbccc"_sz.partition("bb").match.size() == 2);
     assert("abbccc"_sz.partition("bb").after.size() == 3);
     assert("abbccc"_sz.partition("bb").before == "a");
     assert("abbccc"_sz.partition("bb").match == "bb");
     assert("abbccc"_sz.partition("bb").after == "ccc");
+    assert("abb ccc"_sz.partition(sz::whitespaces_set()).after == "ccc");
 
     // Check ranges of search matches
     assert("hello"_sz.find_all("l").size() == 2);
@@ -1313,6 +1432,36 @@ static void test_levenshtein_distances() {
     }
 }
 
+/**
+ *  Evaluates the correctness of look-up table transforms using random lookup tables.
+ *
+ *  @param misalignment The number of bytes to misalign the haystack within the cacheline.
+ */
+void test_replacements(std::size_t lookup_tables_to_try = 128, std::size_t slices_per_table = 256) {
+
+    std::string body, transformed;
+    body.resize(1024 * 1024); // 1MB
+    transformed.resize(1024 * 1024);
+    std::generate(body.begin(), body.end(), []() { return (char)(std::rand() % 256); });
+
+    for (std::size_t lookup_table_variation = 0; lookup_table_variation != lookup_tables_to_try;
+         ++lookup_table_variation) {
+        sz::look_up_table lut;
+        for (std::size_t i = 0; i < 256; i++) lut[(char)i] = (char)(std::rand() % 256);
+
+        for (std::size_t slice_idx = 0; slice_idx != slices_per_table; ++slice_idx) {
+            std::size_t slice_offset = std::rand() % (body.length());
+            std::size_t slice_length = std::rand() % (body.length() - slice_offset);
+
+            sz::transform<char>(sz::string_view(body.data() + slice_offset, slice_length), lut,
+                                const_cast<char *>(transformed.data()) + slice_offset);
+            for (std::size_t i = 0; i != slice_length; ++i) {
+                assert(transformed[slice_offset + i] == lut[body[slice_offset + i]]);
+            }
+        }
+    }
+}
+
 /**
  *  @brief  Tests sorting functionality.
  */
@@ -1353,22 +1502,23 @@ int main(int argc, char const **argv) {
     // Basic utilities
     test_arithmetical_utilities();
     test_memory_utilities();
+    test_replacements();
 
 // Compatibility with STL
 #if SZ_DETECT_CPP_17 && __cpp_lib_string_view
-    test_api_readonly<std::string_view>();
+    test_stl_compatibility_for_reads<std::string_view>();
 #endif
-    test_api_readonly<std::string>();
-    test_api_readonly<sz::string_view>();
-    test_api_readonly<sz::string>();
+    test_stl_compatibility_for_reads<std::string>();
+    test_stl_compatibility_for_reads<sz::string_view>();
+    test_stl_compatibility_for_reads<sz::string>();
 
-    test_api_mutable<std::string>(); // Make sure the test itself is reasonable
-    test_api_mutable<sz::string>();  // The fact that this compiles is already a miracle :)
+    test_stl_compatibility_for_updates<std::string>(); // Make sure the test itself is reasonable
+    test_stl_compatibility_for_updates<sz::string>();  // The fact that this compiles is already a miracle :)
 
     // Cover the non-STL interfaces
-    test_api_readonly_extensions<sz::string_view>();
-    test_api_readonly_extensions<sz::string>();
-    test_api_mutable_extensions();
+    test_non_stl_extensions_for_reads<sz::string_view>();
+    test_non_stl_extensions_for_reads<sz::string>();
+    test_non_stl_extensions_for_updates();
 
     // The string class implementation
     test_constructors();
@@ -1377,7 +1527,7 @@ int main(int argc, char const **argv) {
     test_updates();
 
     // Advanced search operations
-    test_stl_conversion_api();
+    test_stl_conversions();
     test_comparisons();
     test_search();
 #if SZ_DETECT_CPP_17 && __cpp_lib_string_view
diff --git a/scripts/test.hpp b/scripts/test.hpp
index 83f8f42c..9f9abe6b 100644
--- a/scripts/test.hpp
+++ b/scripts/test.hpp
@@ -36,13 +36,15 @@ inline std::mt19937 &global_random_generator() {
  *          The alphabet size is the number of distinct characters in the distribution.
  *
  *  We can't use `std::uniform_int_distribution<char>` because `char` overload is not supported by some platforms.
- *  MSVC, for example, requires one of short, int, long, long long, unsigned short, unsigned int, unsigned long,
- *  or unsigned long long
+ *  MSVC, for example, requires one of `short`, `int`, `long`, `long long`, `unsigned short`, `unsigned int`,
+ *  `unsigned long`, or `unsigned long long`.
  */
 struct uniform_uint8_distribution_t {
     std::uniform_int_distribution<std::uint32_t> distribution;
     inline uniform_uint8_distribution_t(std::size_t alphabet_size = 255)
         : distribution(1, static_cast<std::uint32_t>(alphabet_size)) {}
+    inline uniform_uint8_distribution_t(char from, char to)
+        : distribution(static_cast<std::uint32_t>(from), static_cast<std::uint32_t>(to)) {}
     template <typename generator_type>
     std::uint8_t operator()(generator_type &&generator) {
         return static_cast<std::uint8_t>(distribution(generator));