diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 # Revision history for cube-hs
 
-0.5.0.0
+## 0.5.1.0
+- Support for symmetric pruning table & customized table directory
+- Upgrade underlying C++ library to v0.5.1
+
+## 0.5.0.0
 - Upgrade underlying C++ library to improve performance 
 
 ## 0.3.0 -- YYYY-mm-dd
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -50,7 +50,10 @@
     let scrambledCube = foldl (&>) solvedCube turns
     putStrLn $ "Scrambled Cube:     " ++ showCube scrambledCube
 
-    -- 4. Solve the scrambled cube & verify
+    -- 4a. build tables (`False` means without symmetry) under "cache" directory 
+    cubeInit False "cache"  
+
+    -- 4b. Solve the scrambled cube & verify solution
     let solution = unsafeSolveFrom scrambledCube
     let restoredCube = applyTurns scrambledCube solution
 
diff --git a/cbits/cube_amalg.min.cpp b/cbits/cube_amalg.min.cpp
--- a/cbits/cube_amalg.min.cpp
+++ b/cbits/cube_amalg.min.cpp
@@ -2,1795 +2,2216 @@
  * File: cube_amalg.min.cpp
  * Project: cube
  * Author: coshz <fsinhx@gmail.com>
- * Version: v0.5.0
- * Date: 2026-10-10
- * Homepage: https://github.com/coshz/cube
- * License: MIT
- *
- * Copyright (c) 2026 coshz <fsinhx@gmail.com>. All rights reserved.
- */
-
-#include <cstddef>
-#include <vector>
-#include <array>
-#include <algorithm>
-#include <utility>
-#include <numeric>
-#include <stdexcept>
-#include <initializer_list>
-#include <cassert>
-#include <cstdint>
-#include <string_view>
-#include <set>
-#include <filesystem>
-#include <type_traits>
-#include <limits>
-#include <fstream>
-#include <ios>
-#include <tuple>
-#include <cstdlib>
-#include <chrono>
-#include <optional>
-#include <string>
-#include <cstring>
-#include <regex>
-#include <sstream>
-#include <stdbool.h>
-#include <stdint.h>
-
-namespace cube::math {
-using std::size_t;
-template<typename VectorLike>
-auto decomposite(const VectorLike &xs) -> std::pair<
-    std::vector<typename VectorLike::value_type>,
-    std::vector<std::vector<typename VectorLike::value_type>>
->
-{
-    using T = typename VectorLike::value_type;
-    std::vector<T> fixed;
-    std::vector<std::vector<T>> cycles;
-    std::vector<bool> visited(xs.size(),false);
-    for(int i = 0; i < xs.size(); ++i) {
-        if(visited[i]) continue;
-        std::vector<T> cycle;
-        for(int j = xs[i]; j != i; j = xs[j]) {
-            visited[j] = true;
-            cycle.push_back(static_cast<T>(j));
-        }
-        visited[i] = true;
-        cycle.push_back(static_cast<T>(i));
-        if(cycle.size() > 1) cycles.push_back(cycle);
-        else fixed.push_back(cycle[0]);
-    }
-    std::sort(cycles.begin(), cycles.end(), [](auto &v1, auto &v2){
-        return v1.size() > v2.size();
-    });
-    return std::make_pair(fixed,cycles);
-}
-constexpr size_t factorial(size_t n)
-{
-    if(n>=21) throw std::invalid_argument("factorial(n) overflows for n >= 21");
-    return n == 0 ? 1 : n * factorial(n-1);
-}
-constexpr size_t binomial(size_t n, size_t k)
-{
-    if(n < k)   return 0;
-    if(n == k)  return 1;
-    size_t r = 1, m = std::min(k, n-k);
-    for(size_t i = 1; i <= m; i++) r *= n-i+1;
-    for(size_t i = 1; i <= m; i++) r /= i;
-    return r;
-};
-template<typename T, size_t N>
-size_t orderOf(const std::array<T,N> &xs)
-{
-    size_t m = 1;
-    auto cs = decomposite(xs);
-    for(size_t i = 1; i < cs.size(); i++) { m = std::lcm(m, cs[i].size()); }
-    return m;
-}
-template<typename T, size_t N>
-constexpr size_t rankOf(const std::array<T,N> &xs)
-{
-    size_t r = 0;
-    std::array<bool,N> used {false};
-    for(size_t i = 0; i < N; i++)
-    {
-        int cnt = 0;
-        for(int j=0; j < xs[i]; j++) if(!used[j]) cnt++;
-        r += cnt * factorial(N - i - 1);
-        used[xs[i]] = true;
-    }
-    return r;
-}
-template<typename T, size_t N>
-constexpr std::array<T,N> fromRank(size_t r)
-{
-    std::array<T,N> A{};
-    std::array<bool,N> used {false};
-    for(size_t i = 0; i < N; i++)
-    {
-        size_t f = factorial(N - i - 1);
-        size_t cnt = r / f;
-        r %= f;
-        for(size_t j = 0; j < N; j++)
-        {
-            if(used[j]) continue;
-            if(cnt-- == 0) {
-                A[i] = j;
-                used[j] = true;
-                break;
-            }
-        }
-    }
-    return A;
-}
-template<typename F, typename T, size_t N>
-constexpr std::array<T,N+1> nestList(F &&f, T &&x)
-{
-    std::array<T,N+1> r;
-    r[0] = x;
-    for(auto i = 1; i <= N; i++) r[i] = f(r[i-1]);
-    return r;
-}
-template<size_t Begin, size_t End, typename T, size_t N>
-constexpr auto takeByRange(const std::array<T,N>& xs) -> std::array<T,End-Begin+1>
-{
-    static_assert(Begin <= End && End < N);
-    std::array<T,End-Begin+1> ys;
-    std::copy(xs.begin()+Begin,xs.begin()+End+1,ys.begin());
-    return ys;
-}
-template<size_t B, typename Int, size_t N>
-constexpr auto fromDigits(const std::array<Int,N> &xs) -> size_t
-{
-    auto pow = [](size_t base, size_t exp) -> size_t {
-        size_t prod = 1;
-        for(size_t i = 0; i < exp; i++) prod *= base;
-        return prod;
-    };
-    size_t res = 0;
-    for(size_t i = 0; i < N; i++) res += static_cast<size_t>(xs[i]) * pow(B, N-i-1);
-    return res;
-}
-template<size_t B, size_t N, typename Int>
-constexpr auto toDigits(size_t i) -> std::array<Int,N>
-{
-    auto pow = [](size_t base, size_t exp) -> size_t {
-        size_t prod = 1;
-        for(size_t i = 0; i < exp; i++) prod *= base;
-        return prod;
-    };
-    std::array<Int,N> a {};
-    for(size_t k = 0; k < N; k++) {
-        size_t z = pow(B, N-1-k);
-        a[k] = static_cast<Int>(i / z);
-        i = i % z;
-    }
-    return a;
-}
-template<size_t N, size_t M>
-constexpr auto lexicalOrderFromIndices(const std::array<size_t,M> &X) -> int
-{
-    static_assert(M<=N);
-    int rank = 0;
-    for(size_t i = 0; i < M; i++) rank += binomial(N-1-X[i],M-i);
-    return rank;
-}
-template<size_t N, size_t M>
-constexpr auto lexicalOrderToIndices(int rank) -> std::array<size_t,M>
-{
-    static_assert(M<=N);
-    std::array<size_t,M> X {};
-    size_t n=rank, k=0;
-    for(size_t i = 0; i < M; i++) {
-        while(binomial(N-1-k,M-i) > n || n >= binomial(N-k,M-i)) k++;
-        X[i] = k;
-        n -= binomial(N-1-k,M-i);
-        k += 1;
-    }
-    return X;
-}
-template<size_t N, typename ArrayLike>
-constexpr bool isValidPermutation(const ArrayLike& xs)
-{
-    if(xs.size() != N) return false;
-    std::array<bool,N> visited{};
-    for(auto i = 0; i < N; ++i) {
-        size_t val = static_cast<size_t>(xs[i]);
-        if(xs[i] < 0 || xs[i] >= N) return false;
-        if(visited[val]) return false;
-        visited[val] = true;
-    }
-    return true;
-}
-template<typename Array, typename Perm>
-constexpr Array backpermute(const Array& src, const Perm& P)
-{
-    Array out{src};
-    for (size_t i = 0; i < P.size(); ++i) { out[i] = src[P[i]]; }
-    return out;
-}
-template<typename Array, typename Perm>
-constexpr Array forepermute(const Array& src, const Perm& P)
-{
-    Array out{src};
-    for (size_t i = 0; i < P.size(); ++i) { out[P[i]] = src[i]; }
-    return out;
-}
-}
-namespace cube::data {
-enum Face       { U1,U2,U3,U4,U5,U6,U7,U8,U9,R1,R2,R3,R4,R5,R6,R7,R8,R9,F1,F2,F3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,D7,D8,D9,L1,L2,L3,L4,L5,L6,L7,L8,L9,B1,B2,B3,B4,B5,B6,B7,B8,B9 };
-enum Layer      { U,R,F,D,L,B };
-enum TurnMove   { Ux1,Ux2,Ux3,Rx1,Rx2,Rx3,Fx1,Fx2,Fx3,Dx1,Dx2,Dx3,Lx1,Lx2,Lx3,Bx1,Bx2,Bx3 };
-enum Symmetry   { S_URF3,S_F2,S_U4,S_LR2 };
-enum ColorIndex { UCol,RCol,FCol,DCol,LCol,BCol,NoCol };
-enum Corner     { URF,UFL,ULB,UBR,DFR,DLF,DBL,DRB };
-enum Edge       { UR,UF,UL,UB,DR,DF,DL,DB,FR,FL,BL,BR };
-struct OrientedCorner   { Corner c; unsigned o; };
-struct OrientedEdge     { Edge e;   unsigned o; };
-typedef ColorIndex      SingleFace[9];
-typedef ColorIndex      CornerColorIndex[8][3];
-typedef ColorIndex      EdgeColorIndex[12][2];
-typedef ColorIndex      FaceletColor[54];
-typedef Face            CentreFacelet[6];
-typedef Face            CornerFacelet[8][3];
-typedef Face            EdgeFacelet[12][2];
-typedef Face            Facelet[54];
-typedef OrientedCorner  CornerCubie[8];
-typedef OrientedEdge    EdgeCubie[12];
-typedef Corner          EdgeNeighbour[12][2];
-const CornerColorIndex  CCI = {{UCol,RCol,FCol},{UCol,FCol,LCol},{UCol,LCol,BCol},{UCol,BCol,RCol},{DCol,FCol,RCol},{DCol,LCol,FCol},{DCol,BCol,LCol},{DCol,RCol,BCol}};
-const EdgeColorIndex    ECI = {{UCol,RCol},{UCol,FCol},{UCol,LCol},{UCol,BCol},{DCol,RCol},{DCol,FCol}, {DCol,LCol},{DCol,BCol},{FCol,RCol},{FCol,LCol},{BCol,LCol},{BCol,RCol}};
-const CentreFacelet     CC  = {U5,R5,F5,D5,L5,B5};
-const CornerFacelet     CF  = {{U9,R1,F3},{U7,F1,L3},{U1,L1,B3},{U3,B1,R3},{D3,F9,R7},{D1,L9,F7},{D7,B9,L7},{D9,R9,B7}};
-const EdgeFacelet       EF  = {{U6,R2},{U8,F2},{U4,L2},{U2,B2},{D6,R8},{D2,F8}, {D4,L8},{D8,B8},{F6,R4},{F4,L6},{B6,L4},{B4,R6}};
-const EdgeNeighbour     EN  = {{URF,UBR},{UFL,URF},{ULB,UFL},{UBR,ULB},{DRB,DFR},{DFR,DLF}, {DLF,DBL},{DBL,DRB},{URF,DFR},{DLF,UFL},{DBL,ULB},{UBR,DRB}};
-const Facelet FaceletMove[6] = {
-    {U3,U6,U9,U2,U5,U8,U1,U4,U7,F1,F2,F3,R4,R5,R6,R7,R8,R9,L1,L2,L3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,D7,D8,D9,B1,B2,B3,L4,L5,L6,L7,L8,L9,R1,R2,R3,B4,B5,B6,B7,B8,B9},
-    {U1,U2,B7,U4,U5,B4,U7,U8,B1,R3,R6,R9,R2,R5,R8,R1,R4,R7,F1,F2,U3,F4,F5,U6,F7,F8,U9,D1,D2,F3,D4,D5,F6,D7,D8,F9,L1,L2,L3,L4,L5,L6,L7,L8,L9,D9,B2,B3,D6,B5,B6,D3,B8,B9},
-    {U1,U2,U3,U4,U5,U6,R1,R4,R7,D3,R2,R3,D2,R5,R6,D1,R8,R9,F3,F6,F9,F2,F5,F8,F1,F4,F7,L3,L6,L9,D4,D5,D6,D7,D8,D9,L1,L2,U9,L4,L5,U8,L7,L8,U7,B1,B2,B3,B4,B5,B6,B7,B8,B9},
-    {U1,U2,U3,U4,U5,U6,U7,U8,U9,R1,R2,R3,R4,R5,R6,B7,B8,B9,F1,F2,F3,F4,F5,F6,R7,R8,R9,D3,D6,D9,D2,D5,D8,D1,D4,D7,L1,L2,L3,L4,L5,L6,F7,F8,F9,B1,B2,B3,B4,B5,B6,L7,L8,L9},
-    {F1,U2,U3,F4,U5,U6,F7,U8,U9,R1,R2,R3,R4,R5,R6,R7,R8,R9,D1,F2,F3,D4,F5,F6,D7,F8,F9,B9,D2,D3,B6,D5,D6,B3,D8,D9,L3,L6,L9,L2,L5,L8,L1,L4,L7,B1,B2,U7,B4,B5,U4,B7,B8,U1},
-    {L7,L4,L1,U4,U5,U6,U7,U8,U9,R1,R2,U1,R4,R5,U2,R7,R8,U3,F1,F2,F3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,R9,R6,R3,D7,L2,L3,D8,L5,L6,D9,L8,L9,B3,B6,B9,B2,B5,B8,B1,B4,B7}
-};
-const Facelet FaceletSym[4] = {
-    {R9,R8,R7,R6,R5,R4,R3,R2,R1,F3,F6,F9,F2,F5,F8,F1,F4,F7,U3,U6,U9,U2,U5,U8,U1,U4,U7,L1,L2,L3,L4,L5,L6,L7,L8,L9,B7,B4,B1,B8,B5,B2,B9,B6,B3,D3,D6,D9,D2,D5,D8,D1,D4,D7},
-    {D9,D8,D7,D6,D5,D4,D3,D2,D1,L9,L8,L7,L6,L5,L4,L3,L2,L1,F9,F8,F7,F6,F5,F4,F3,F2,F1,U9,U8,U7,U6,U5,U4,U3,U2,U1,R9,R8,R7,R6,R5,R4,R3,R2,R1,B9,B8,B7,B6,B5,B4,B3,B2,B1},
-    {U3,U6,U9,U2,U5,U8,U1,U4,U7,F1,F2,F3,F4,F5,F6,F7,F8,F9,L1,L2,L3,L4,L5,L6,L7,L8,L9,D7,D4,D1,D8,D5,D2,D9,D6,D3,B1,B2,B3,B4,B5,B6,B7,B8,B9,R1,R2,R3,R4,R5,R6,R7,R8,R9},
-    {U3,U2,U1,U6,U5,U4,U9,U8,U7,L3,L2,L1,L6,L5,L4,L9,L8,L7,F3,F2,F1,F6,F5,F4,F9,F8,F7,D3,D2,D1,D6,D5,D4,D9,D8,D7,R3,R2,R1,R6,R5,R4,R9,R8,R7,B3,B2,B1,B6,B5,B4,B9,B8,B7}
-};
-const CornerCubie CornerCubieMove[6] = {
-    {{UBR,0},{URF,0},{UFL,0},{ULB,0},{DFR,0},{DLF,0},{DBL,0},{DRB,0}},
-    {{DFR,2},{UFL,0},{ULB,0},{URF,1},{DRB,1},{DLF,0},{DBL,0},{UBR,2}},
-    {{UFL,1},{DLF,2},{ULB,0},{UBR,0},{URF,2},{DFR,1},{DBL,0},{DRB,0}},
-    {{URF,0},{UFL,0},{ULB,0},{UBR,0},{DLF,0},{DBL,0},{DRB,0},{DFR,0}},
-    {{URF,0},{ULB,1},{DBL,2},{UBR,0},{DFR,0},{UFL,2},{DLF,1},{DRB,0}},
-    {{URF,0},{UFL,0},{UBR,1},{DRB,2},{DFR,0},{DLF,0},{ULB,2},{DBL,1}}
-};
-const EdgeCubie EdgeCubieMove[6] = {
-    {{UB,0},{UR,0},{UF,0},{UL,0},{DR,0},{DF,0},{DL,0},{DB,0},{FR,0},{FL,0},{BL,0},{BR,0}},
-    {{FR,0},{UF,0},{UL,0},{UB,0},{BR,0},{DF,0},{DL,0},{DB,0},{DR,0},{FL,0},{BL,0},{UR,0}},
-    {{UR,0},{FL,1},{UL,0},{UB,0},{DR,0},{FR,1},{DL,0},{DB,0},{UF,1},{DF,1},{BL,0},{BR,0}},
-    {{UR,0},{UF,0},{UL,0},{UB,0},{DF,0},{DL,0},{DB,0},{DR,0},{FR,0},{FL,0},{BL,0},{BR,0}},
-    {{UR,0},{UF,0},{BL,0},{UB,0},{DR,0},{DF,0},{FL,0},{DB,0},{FR,0},{UL,0},{DL,0},{BR,0}},
-    {{UR,0},{UF,0},{UL,0},{BR,1},{DR,0},{DF,0},{DL,0},{BL,1},{FR,0},{FL,0},{UB,1},{DB,1}}
-};
-const CornerCubie CornerCubieSym[4] = {
-    {{URF,1},{DFR,2},{DLF,1},{UFL,2},{UBR,2},{DRB,1},{DBL,2},{ULB,1}},
-    {{DLF,0},{DFR,0},{DRB,0},{DBL,0},{UFL,0},{URF,0},{UBR,0},{ULB,0}},
-    {{UBR,0},{URF,0},{UFL,0},{ULB,0},{DRB,0},{DFR,0},{DLF,0},{DBL,0}},
-    {{UFL,3},{URF,3},{UBR,3},{ULB,3},{DLF,3},{DFR,3},{DRB,3},{DBL,3}}
-};
-const EdgeCubie EdgeCubieSym[4] = {
-    {{UF,1},{FR,0},{DF,1},{FL,0},{UB,1},{BR,0},{DB,1},{BL,0},{UR,1},{DR,1},{DL,1},{UL,1}},
-    {{DL,0},{DF,0},{DR,0},{DB,0},{UL,0},{UF,0},{UR,0},{UB,0},{FL,0},{FR,0},{BR,0},{BL,0}},
-    {{UB,0},{UR,0},{UF,0},{UL,0},{DB,0},{DR,0},{DF,0},{DL,0},{BR,1},{FR,1},{FL,1},{BL,1}},
-    {{UL,0},{UF,0},{UR,0},{UB,0},{DL,0},{DF,0},{DR,0},{DB,0},{FL,0},{FR,0},{BR,0},{BL,0}}
-};
-enum Constant {
-    GN_HTM      = 20,
-    GN_QTM      = 26,
-    N_MOVE      = 18,
-    N_TWIST     = 2187,
-    N_FLIP      = 2048,
-    N_SLICE     = 495,
-    N_CORNER    = 40320,
-    N_EDGE8     = 40320,
-    N_EDGE4     = 24,
-    N_SYM       = 48,
-    N_SYM_D4h   = 16,
-    EQ_FLIPSLICE= 64430,
-    EQ_CORNER   = 2768,
-};
-using std::size_t;
-using namespace cube::math;
-template<size_t N,typename T=int>
-struct Perm
-{
-    using value_type = T;
-    static constexpr size_t size() { return N; }
-    constexpr Perm() noexcept
-    {
-        for(size_t i = 0; i < N; ++i) X[i] = static_cast<T>(i);
-    }
-    constexpr Perm(const std::array<T,N>& xs) noexcept
-    :X{xs}
-    {
-        assert(isValidPermutation<N>(xs) && "array must be a valid permutation");
-    }
-    constexpr Perm(std::initializer_list<T> xs) noexcept
-    {
-        assert(xs.size() == N && "size mismatch");
-        auto dit = X.begin();
-        for(auto it = xs.begin(); it != xs.end(); it++, dit++) { *dit = *it; }
-        assert(isValidPermutation<N>(X) && "invalid permutation list");
-    }
-    template<typename ArrayLike>
-    constexpr ArrayLike act_right(const ArrayLike& src) const noexcept
-    {
-        return backpermute(src,this->X);
-    }
-    friend constexpr Perm operator*(const Perm &P, const Perm& Q) noexcept
-    {
-        return Q.act_right(P);
-    }
-    constexpr Perm& operator*=(const Perm& rhs) noexcept
-    {
-        return *this = *this * rhs;
-    }
-    friend constexpr bool operator==(const Perm &lhs, const Perm& rhs) noexcept
-    {
-        return lhs.X == rhs.X;
-    }
-    constexpr Perm operator~() const
-    {
-        return { forepermute(Perm::id.X, this->X) };
-    }
-    constexpr T& operator[](size_t idx)             { return X[idx]; }
-    constexpr const T& operator[](size_t idx) const { return X[idx]; }
-    bool parity() const
-    {
-        const auto [fixed, cs] = decomposite(this->X);
-        int s = 1;
-        for(auto &c:cs) s *= (c.size() % 2 == 0) ? -1 : 1;
-        return s == 1;
-    }
-    size_t order() const { return orderOf(X); }
-    constexpr size_t rank() const { return rankOf(X); }
-    static constexpr Perm<N,T> fromRank(size_t i)
-    { return { cube::math::fromRank<T,N>(i) }; }
-    static const Perm id;
-    std::array<T,N> X{};
-};
-template<size_t N,typename T>
-inline constexpr Perm<N,T> Perm<N,T>::id{};
-template<size_t N, size_t L, typename T=int>
-struct CArray
-{
-    using value_type = T;
-    static constexpr size_t period = N;
-    static constexpr size_t size() noexcept { return L; }
-    constexpr CArray() noexcept {}
-    constexpr CArray(const std::array<T,L> &arr) noexcept
-    :xs{arr}
-    {
-        auto n = static_cast<T>(N);
-        for(size_t i = 0; i < L; i++) { xs[i] = (xs[i] % n + n) % n; }
-    }
-    constexpr CArray(std::initializer_list<T> list) noexcept
-    {
-        auto n = static_cast<T>(N);
-        std::size_t i = 0;
-        for(T val : list) { if(i < L) { xs[i++] = (val % n + n) % n; } }
-    }
-    constexpr T& operator[](size_t idx) noexcept             { return xs[idx]; }
-    constexpr const T& operator[](size_t idx) const noexcept { return xs[idx]; }
-    friend constexpr bool operator==(const CArray& lhs, const CArray& rhs) noexcept
-    {
-        return lhs.xs == rhs.xs;
-    }
-    constexpr CArray& operator+=(const CArray& rhs)
-    {
-        for(size_t i = 0; i < L; i++) xs[i] = (xs[i] + rhs.xs[i]) % N;
-        return *this;
-    }
-    friend constexpr CArray operator+(CArray lhs, const CArray& rhs)
-    {
-        return lhs += rhs;
-    }
-    constexpr CArray operator~() const noexcept
-    {
-        CArray<N,L,T> inv {};
-        auto n = static_cast<T>(N);
-        for(size_t i = 0; i < L; i++) inv[i] = (n - xs[i]) % n;
-        return inv;
-    }
-    constexpr T sum() const noexcept
-    {
-        T s = 0;
-        for(size_t i = 0; i < L; i++) s += xs[i];
-        return s % N;
-    }
-    static const CArray id;
-    std::array<T,L> xs {};
-};
-template<size_t N, size_t L, typename T>
-inline constexpr CArray<N,L,T> CArray<N,L,T>::id{};
-template<size_t N, size_t L, typename T>
-constexpr CArray<N,L,T> operator*(const CArray<N,L,T> &xs, const Perm<L,T> &P)
-{
-    return P.act_right(xs);
-}
-template<size_t N, size_t L, typename T>
-constexpr CArray<N,L,T>& operator*=(CArray<N,L,T> &xs, const Perm<L,T> &P)
-{
-    return xs = P.act_right(xs);
-}
-template<typename T, size_t N, size_t... Ns>
-struct ArrayHelper {
-    using type = std::array<typename ArrayHelper<T,Ns...>::type,N>;
-};
-template<typename T, size_t N>
-struct ArrayHelper<T,N> {
-    using type = std::array<T,N>;
-};
-template<typename T, size_t... Ns>
-struct NArray
-{
-    using value_type = T;
-    static constexpr size_t size = (Ns * ...);
-    static constexpr size_t dim = sizeof...(Ns);
-    static constexpr std::array<size_t,dim> shape { Ns... };
-    typename ArrayHelper<T,Ns...>::type data;
-    auto & operator[](size_t i) {
-        return data[i];
-    }
-    const auto & operator[](size_t i) const {
-        return data[i];
-    }
-};
-}
-namespace cube {
-using namespace cube::data;
-typedef std::int8_t cube_value_t;
-typedef Perm<54,    cube_value_t>   FacePerm;
-typedef Perm<8,     cube_value_t>   CornerPerm;
-typedef Perm<12,    cube_value_t>   EdgePerm;
-typedef CArray<3, 8,cube_value_t>   CornerOri;
-typedef CArray<2,12,cube_value_t>   EdgeOri;
-struct ColorState;
-struct FaceCube;
-struct CubieCube;
-constexpr CornerPerm operator*(const CornerPerm &cp, const CubieCube &cc);
-constexpr CornerOri  operator*(const CornerOri  &co, const CubieCube &cc);
-constexpr EdgePerm   operator*(const EdgePerm   &ep, const CubieCube &cc);
-constexpr EdgeOri    operator*(const EdgeOri    &eo, const CubieCube &cc);
-struct ColorState
-{
-    std::array<Layer,54> s;
-    Layer & operator[](size_t i) noexcept           { return s[i]; }
-    const Layer operator[](size_t i) const noexcept { return s[i]; }
-    static ColorState fromString(std::string_view cube);
-    static bool is_valid_config(std::string_view cube);
-    FaceCube  toFaceCube() const;
-    CubieCube toCubieCube() const;
-    static const ColorState id;
-};
-inline const ColorState ColorState::id = {{U,U,U,U,U,U,U,U,U,R,R,R,R,R,R,R,R,R,F,F,F,F,F,F,F,F,F,D,D,D,D,D,D,D,D,D,L,L,L,L,L,L,L,L,L,B,B,B,B,B,B,B,B,B}};
-struct FaceCube
-{
-    FacePerm f;
-    CubieCube toCubieCube() const;
-    friend constexpr FaceCube operator*(const FaceCube &lhs, const FaceCube &rhs)
-    { return { lhs.f * rhs.f }; }
-    constexpr FaceCube& operator*=(const FaceCube &rhs)
-    { return *this = *this * rhs; }
-    constexpr FaceCube operator~() const
-    { return { ~f }; }
-    friend bool operator==(const FaceCube &lhs, const FaceCube &rhs)
-    { return lhs.f == rhs.f; }
-    static const FaceCube id;
-};
-struct CubieCube
-{
-    CornerPerm  cp;
-    CornerOri   co;
-    EdgePerm    ep;
-    EdgeOri     eo;
-    FaceCube toFaceCube() const;
-    bool isSolvable() const
-    {  return cp.parity() == ep.parity() && co.sum() == 0 && eo.sum() == 0; }
-    friend constexpr CubieCube operator*(const CubieCube &a, const CubieCube &b)
-    { return { a.cp*b, a.co*b, a.ep*b, a.eo*b }; }
-    constexpr CubieCube& operator*=(const CubieCube &rhs)
-    { return *this = *this * rhs; }
-    constexpr CubieCube operator~() const
-    {
-        auto cp_inv = ~cp;
-        auto ep_inv = ~ep;
-        return { cp_inv, ~(co*cp_inv), ep_inv, ~(eo*ep_inv) };
-    }
-    friend bool operator==(const CubieCube &lhs, const CubieCube &rhs)
-    { return lhs.cp == rhs.cp && lhs.co == rhs.co && lhs.ep == rhs.ep && lhs.eo == rhs.eo; }
-    static const CubieCube id;
-};
-inline constexpr CornerPerm operator*(const CornerPerm &cp, const CubieCube &cc) { return cp * cc.cp;}
-inline constexpr CornerOri  operator*(const CornerOri &co,  const CubieCube &cc) { return co * cc.cp + cc.co; }
-inline constexpr EdgePerm   operator*(const EdgePerm &ep,   const CubieCube &cc) { return ep * cc.ep; }
-inline constexpr EdgeOri    operator*(const EdgeOri &eo,    const CubieCube &cc) { return eo * cc.ep + cc.eo; }
-inline constexpr CornerPerm    eCP = {0,1,2,3,4,5,6,7};
-inline constexpr EdgePerm      eEP = {0,1,2,3,4,5,6,7,8,9,10,11};
-inline constexpr CornerOri     eCO = {0,0,0,0,0,0,0,0};
-inline constexpr EdgeOri       eEO = {0,0,0,0,0,0,0,0,0,0,0,0};
-inline constexpr FacePerm      eFP = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53};
-inline const FaceCube   FaceCube::id = FaceCube{eFP};
-inline const CubieCube  CubieCube::id = CubieCube{eCP,eCO,eEP,eEO};
-inline constexpr CubieCube mU = {{3,0,1,2,4,5,6,7},{0,0,0,0,0,0,0,0},{3,0,1,2,4,5,6,7,8,9,10,11},{0,0,0,0,0,0,0,0,0,0,0,0}};
-inline constexpr CubieCube mR = {{4,1,2,0,7,5,6,3},{2,0,0,1,1,0,0,2},{8,1,2,3,11,5,6,7,4,9,10,0},{0,0,0,0,0,0,0,0,0,0,0,0}};
-inline constexpr CubieCube mF = {{1,5,2,3,0,4,6,7},{1,2,0,0,2,1,0,0},{0,9,2,3,4,8,6,7,1,5,10,11},{0,1,0,0,0,1,0,0,1,1,0,0}};
-inline constexpr CubieCube mD = {{0,1,2,3,5,6,7,4},{0,0,0,0,0,0,0,0},{0,1,2,3,5,6,7,4,8,9,10,11},{0,0,0,0,0,0,0,0,0,0,0,0}};
-inline constexpr CubieCube mL = {{0,2,6,3,4,1,5,7},{0,1,2,0,0,2,1,0},{0,1,10,3,4,5,9,7,8,2,6,11},{0,0,0,0,0,0,0,0,0,0,0,0}};
-inline constexpr CubieCube mB = {{0,1,3,7,4,5,2,6},{0,0,1,2,0,0,2,1},{0,1,2,11,4,5,6,10,8,9,3,7},{0,0,0,1,0,0,0,1,0,0,1,1}};
-inline constexpr std::array<CubieCube,18>
-    ElementaryMove = { mU,mU*mU,mU*mU*mU,mR,mR*mR,mR*mR*mR,mF,mF*mF,mF*mF*mF,mD,mD*mD,mD*mD*mD,mL,mL*mL,mL*mL*mL,mB,mB*mB,mB*mB*mB };
-inline constexpr FaceCube pU = {{6,3,0,7,4,1,8,5,2,45,46,47,12,13,14,15,16,17,9,10,11,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,18,19,20,39,40,41,42,43,44,36,37,38,48,49,50,51,52,53}};
-inline constexpr FaceCube pR = {{0,1,20,3,4,23,6,7,26,15,12,9,16,13,10,17,14,11,18,19,29,21,22,32,24,25,35,27,28,51,30,31,48,33,34,45,36,37,38,39,40,41,42,43,44,8,46,47,5,49,50,2,52,53}};
-inline constexpr FaceCube pF = {{0,1,2,3,4,5,44,41,38,6,10,11,7,13,14,8,16,17,24,21,18,25,22,19,26,23,20,15,12,9,30,31,32,33,34,35,36,37,27,39,40,28,42,43,29,45,46,47,48,49,50,51,52,53}};
-inline constexpr FaceCube pD = {{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,24,25,26,18,19,20,21,22,23,42,43,44,33,30,27,34,31,28,35,32,29,36,37,38,39,40,41,51,52,53,45,46,47,48,49,50,15,16,17}};
-inline constexpr FaceCube pL = {{53,1,2,50,4,5,47,7,8,9,10,11,12,13,14,15,16,17,0,19,20,3,22,23,6,25,26,18,28,29,21,31,32,24,34,35,42,39,36,43,40,37,44,41,38,45,46,33,48,49,30,51,52,27}};
-inline constexpr FaceCube pB = {{11,14,17,3,4,5,6,7,8,9,10,35,12,13,34,15,16,33,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,36,39,42,2,37,38,1,40,41,0,43,44,51,48,45,52,49,46,53,50,47}};
-inline constexpr std::array<FaceCube,18>
-    ElementaryPerm = { pU,pU*pU,pU*pU*pU,pR,pR*pR,pR*pR*pR,pF,pF*pF,pF*pF*pF,pD,pD*pD,pD*pD*pD,pL,pL*pL,pL*pL*pL,pB,pB*pB,pB*pB*pB };
-inline FaceCube operator*(const FaceCube &c, const std::vector<TurnMove> &ms)
-{
-    FaceCube fc = c;
-    for (auto m : ms) fc = fc * ElementaryPerm[m];
-    return fc;
-}
-inline CubieCube operator*(const CubieCube &c, const std::vector<TurnMove> &ms)
-{
-    CubieCube cc = c;
-    for (auto m : ms) cc = cc * ElementaryMove[m];
-    return cc;
-}
-inline ColorState operator*(const ColorState &c, const std::vector<TurnMove> &ms)
-{
-    ColorState cs = c;
-    for (auto m : ms) cs = ElementaryPerm[m].f.act_right(cs);
-    return cs;
-}
-}
-namespace cube {
-using namespace cube::data;
-ColorState ColorState::fromString(std::string_view cube)
-{
-    assert(is_valid_config(cube) && "invalid cube config");
-    std::array<Layer,54> xs{};
-    std::transform(cube.cbegin(), cube.cend(), xs.begin(), [](char c) {
-        switch(c) {
-        case 'U': return U;
-        case 'R': return R;
-        case 'F': return F;
-        case 'D': return D;
-        case 'L': return L;
-        case 'B': return B;
-        default: throw std::invalid_argument("invalid cube config");
-        }
-    });
-    return { xs };
-}
-bool ColorState::is_valid_config(std::string_view cfg)
-{
-    if(cfg.size() != 54) return false;
-    std::set<char> vs { cfg[CC[0]],cfg[CC[1]],cfg[CC[2]],cfg[CC[3]],cfg[CC[4]],cfg[CC[5]] };
-    if(vs.size() != 6) return false;
-    for(size_t i = 0, x = 0; i < 8; i++) {
-        for(x = 0; x < 24; x++) {
-            if(cfg[CC[CCI[i][0]]] == cfg[CF[x/3][x%3]]
-               && cfg[CC[CCI[i][1]]] == cfg[CF[x/3][(x+1)%3]]
-               && cfg[CC[CCI[i][2]]] == cfg[CF[x/3][(x+2)%3]]) break;
-        }
-        if(x >= 24) return false;
-    }
-    for(size_t i = 0, y = 0; i < 12; i++) {
-        for(y = 0; y < 24; y++) {
-            if(cfg[CC[ECI[i][0]]] == cfg[EF[y/2][y%2]]
-               && cfg[CC[ECI[i][1]]] == cfg[EF[y/2][(y+1)%2]]) break;
-        }
-        if(y >= 24) return false;
-    }
-    return true;
-}
-FaceCube ColorState::toFaceCube() const
-{
-    FacePerm fp{};
-    for(int i = 0; i < 6; ++i) {
-        fp[CC[i]] = s[CC[i]];
-    }
-    for(int i = 0; i < 8; i++) {
-        for(int x = 0; x < 24; x++) {
-            if(s[CC[CCI[i][0]]] == s[CF[x/3][x%3]]
-            && s[CC[CCI[i][1]]] == s[CF[x/3][(x+1)%3]]
-            && s[CC[CCI[i][2]]] == s[CF[x/3][(x+2)%3]])
-            {
-                fp[CF[x/3][x%3]]      = CF[i][0];
-                fp[CF[x/3][(x+1)%3]]  = CF[i][1];
-                fp[CF[x/3][(x+2)%3]]  = CF[i][2];
-                break;
-            }
-        }
-    }
-    for(int i = 0; i < 12; i++) {
-        for(int y = 0; y < 24; y++) {
-            if(s[CC[ECI[i][0]]] == s[EF[y/2][y%2]]
-            && s[CC[ECI[i][1]]] == s[EF[y/2][(y+1)%2]])
-            {
-                fp[EF[y/2][y%2]]      = EF[i][0];
-                fp[EF[y/2][(y+1)%2]]  = EF[i][1];
-                break;
-            }
-        }
-    }
-    return { fp };
-}
-CubieCube ColorState::toCubieCube() const
-{
-    return toFaceCube().toCubieCube();
-}
-CubieCube FaceCube::toCubieCube() const
-{
-    CubieCube cc{};
-    const FaceCube &fc = *this;
-    for(int i = 0; i < 8; i++) {
-        for(int x = 0; x < 24; x++) {
-            if(fc.f[CF[i][0]] == CF[x/3][x%3]) {
-                cc.cp[i] = x/3, cc.co[i] = (3-x%3)%3;
-                break;
-            }
-        }
-    }
-    for(int i = 0; i < 12; i++){
-        for(int y = 0; y < 24; y++){
-            if(fc.f[EF[i][0]] == EF[y/2][y%2]) {
-                cc.ep[i] = y/2, cc.eo[i] = (2-y%2)%2;
-                break;
-            }
-        }
-    }
-    return cc;
-}
-FaceCube CubieCube::toFaceCube() const
-{
-    FaceCube fc{};
-    const CubieCube &cc = *this;
-    for(int i = 0; i < 6; i++) {
-        fc.f[CC[i]] = CC[i];
-    }
-    for(int i = 0; i < 8; i++) for(int j = 0; j < 3; j++) {
-        fc.f[CF[i][j]] = CF[cc.cp[i]][(j-cc.co[i]+3)%3];
-    }
-    for(int i = 0; i < 12; i++) for(int j = 0; j < 2; j++) {
-        fc.f[EF[i][j]] = EF[cc.ep[i]][(j-cc.eo[i]+2)%2];
-    }
-    return fc;
-}
-}
-namespace cube::pdb {
-struct Coord
-{
-    int twist, flip, slice;
-    int corner, edge4, edge8;
-    static const Coord id;
-    static CornerOri    twist2co(int);
-    static int          co2twist(const CornerOri &);
-    static EdgeOri      flip2eo(int);
-    static int          eo2flip(const EdgeOri &);
-    static CornerPerm   corner2cp(int);
-    static int          cp2corner(const CornerPerm &);
-    static int          ep2slice(const EdgePerm &);
-    static int          ep2edge4(const EdgePerm &);
-    static int          ep2edge8(const EdgePerm &);
-    static EdgePerm     slice2ep(int);
-    static EdgePerm     edge42ep(int);
-    static EdgePerm     edge82ep(int);
-    static EdgePerm     see2ep(int,int,int);
-    static Coord        CubieCube2Coord(const CubieCube &);
-    static CubieCube    Coord2CubieCube(const Coord &);
-};
-constexpr bool operator==(const Coord &c1, const Coord &c2)
-{
-    return c1.twist == c2.twist && c1.flip == c2.flip && c1.slice == c2.slice
-        && c1.corner == c2.corner && c1.edge4 == c2.edge4 && c1.edge8 == c2.edge8;
-}
-inline constexpr Coord Coord::id = { 0,0,0,0,0,0 };
-}
-namespace cube::pdb {
-using namespace cube::math;
-using namespace cube::data;
-inline bool isSliceEdge(size_t idx)
-{
-    return FR <= idx && idx <= BR;
-}
-int Coord::co2twist(const CornerOri &co)
-{
-    return static_cast<int>(fromDigits<3>(takeByRange<1,7>(co.xs)));
-}
-CornerOri Coord::twist2co(int i)
-{
-    CornerOri co;
-    auto sub1to7 = CArray<3,7,CornerOri::value_type>{toDigits<3,7,CornerOri::value_type>(i)};
-    std::copy(sub1to7.xs.begin(),sub1to7.xs.end(),co.xs.begin()+1);
-    co[0] = (3 - sub1to7.sum()) % 3;
-    return co;
-}
-int Coord::eo2flip(const EdgeOri &eo)
-{
-    return static_cast<int>(fromDigits<2>(takeByRange<1,11>(eo.xs)));
-}
-EdgeOri Coord::flip2eo(int i)
-{
-    EdgeOri eo;
-    auto sub1to11 = CArray<2,11,CornerOri::value_type>{toDigits<2,11,CornerOri::value_type>(i)};
-    std::copy(sub1to11.xs.begin(),sub1to11.xs.end(),eo.xs.begin()+1);
-    eo[0] = (2 - sub1to11.sum()) % 2;
-    return eo;
-}
-int Coord::ep2slice(const EdgePerm &ep)
-{
-    int x[4],slice=0,N=12;
-    for(int i = 0, j = 0; i < N; i++) {
-        if(isSliceEdge(ep[i])) x[j++] = i;
-    }
-    for(int i = 0; i < 4; i++) slice += binomial(N-1-x[i],4-i);
-    return slice;
-}
-int Coord::ep2edge4(const EdgePerm &ep)
-{
-    Perm<4,EdgePerm::value_type> edge4_perm;
-    for(int i = 0, j = 0; i < 4; i++, j++) {
-        while(!isSliceEdge(ep[j])) j++;
-        edge4_perm[i] = ep[j] - 8;
-    }
-    return static_cast<int>(edge4_perm.rank());
-}
-int Coord::ep2edge8(const EdgePerm &ep)
-{
-    Perm<8,EdgePerm::value_type> edge8_perm;
-    for(size_t i = 0, j = 0; i < 8; i++, j++) {
-        while(isSliceEdge(ep[j]) || ep[j] == (EdgePerm::value_type) ~0UL) j++;
-        edge8_perm[i] = ep[j] - 0;
-    }
-    return static_cast<int>(edge8_perm.rank());
-}
-int Coord::cp2corner(const CornerPerm &cp)
-{
-    return static_cast<int>(cp.rank());
-}
-CornerPerm Coord::corner2cp(int i)
-{
-    return CornerPerm::fromRank(i);
-}
-EdgePerm Coord::slice2ep(int i)
-{
-    auto si = lexicalOrderToIndices<12,4>(i);
-    EdgePerm ep;
-    for(size_t i = 0, j = 0; i < 12; i++){
-        ep[i] = (i == si[j]) ? 8 + j++ : (EdgePerm::value_type) ~0UL;
-    }
-    return ep;
-}
-EdgePerm Coord::edge42ep(int i)
-{
-    EdgePerm ep;
-    auto e4 = Perm<4,EdgePerm::value_type>::fromRank(i);
-    for(size_t i = 0; i < 12; i++){
-        ep[i] = (i < 8) ? (EdgePerm::value_type) ~0UL : e4[i-8] + 8;
-    }
-    return ep;
-}
-EdgePerm Coord::edge82ep(int i)
-{
-    EdgePerm ep;
-    auto e8 = Perm<8,EdgePerm::value_type>::fromRank(i);
-    for(size_t i = 0; i < 12; i++){
-        ep[i] = (i < 8) ? e8[i] + 0 : (EdgePerm::value_type) ~0UL;
-    }
-    return ep;
-}
-EdgePerm Coord::see2ep(int slice, int edge4, int edge8)
-{
-    auto e4 = Perm<4,EdgePerm::value_type>::fromRank(edge4);
-    auto e8 = Perm<8,EdgePerm::value_type>::fromRank(edge8);
-    auto slice_indices = lexicalOrderToIndices<12,4>(slice);
-    EdgePerm ep;
-    for(size_t i = 0, j = 0, x = 0, y = 0; i < 12; i++) {
-        ep[i] = (i == slice_indices[j] && ++j) ? e4[x++]+8: e8[y++]+0;
-    }
-    return ep;
-}
-Coord Coord::CubieCube2Coord(const CubieCube &cc)
-{
-    return {
-        Coord::co2twist(cc.co),
-        Coord::eo2flip(cc.eo),
-        Coord::ep2slice(cc.ep),
-        Coord::cp2corner(cc.cp),
-        Coord::ep2edge4(cc.ep),
-        Coord::ep2edge8(cc.ep)
-    };
-}
-CubieCube Coord::Coord2CubieCube(const Coord &c)
-{
-    return {
-        Coord::corner2cp(c.corner),
-        Coord::twist2co(c.twist),
-        Coord::see2ep(c.slice,c.edge4,c.edge8),
-        Coord::flip2eo(c.flip)
-    };
-}
-}
-namespace cube::config {
-void set_table_dir(std::filesystem::path dir);
-auto get_table_dir() -> std::filesystem::path;
-}
-namespace cube::pdb {
-using namespace cube::data;
-namespace fs = std::filesystem;
-typedef uint16_t    mt_value_t;
-typedef uint8_t     pt_value_t;
-template <typename Table> void save_to(const Table &table, fs::path path);
-template <typename Table> void load_from(Table &table, fs::path path);
-template<class T>
-class Singleton
-{
-public:
-    Singleton(const Singleton&) = delete;
-    Singleton(Singleton&&) = delete;
-    Singleton& operator=(const Singleton&) = delete;
-    Singleton& operator=(Singleton&&) = delete;
-    static T& instance() {
-        static T obj {};
-        return obj;
-    }
-protected:
-    Singleton() = default;
-    ~Singleton() = default;
-};
-template<typename T=mt_value_t>
-struct TableMove
-{
-    static_assert(std::is_integral<T>::value);
-    static_assert(std::numeric_limits<T>::digits >= 16);
-    using value_t = T;
-    TableMove(fs::path dir = {});
-    TableMove(const TableMove &) = delete;
-    ~TableMove();
-    TableMove& operator=(const TableMove &) = delete;
-    template<typename Table, typename F1, typename F2>
-    std::enable_if_t<Table::shape[0] == N_MOVE, void>
-    buildMoveTable(Table &t, F1&& coord2i, F2&& i2coord, fs::path filename);
-    static bool is_ready(fs::path);
-    const fs::path tdir;
-    NArray<T,N_MOVE,N_TWIST>   *pTMTwist;
-    NArray<T,N_MOVE,N_FLIP>    *pTMFlip;
-    NArray<T,N_MOVE,N_SLICE>   *pTMSlice;
-    NArray<T,N_MOVE,N_CORNER>  *pTMCorner;
-    NArray<T,N_MOVE,N_EDGE4>   *pTMEdge4;
-    NArray<T,N_MOVE,N_EDGE8>   *pTMEdge8;
-};
-template<typename T=pt_value_t>
-struct TablePrunning
-{
-    using value_type = T;
-    TablePrunning(fs::path dir = {});
-    TablePrunning(const TablePrunning &) = delete;
-    ~TablePrunning();
-    TablePrunning operator=(const TablePrunning &) = delete;
-    template<typename Table, typename MT1, typename MT2>
-    std::enable_if_t<Table::shape[0] == MT1::shape[1] && Table::shape[1] == MT2::shape[1]>
-    buildPrunningTable(Table &t, const MT1 &mt1, const MT2 &mt2, fs::path filename);
-    static bool is_ready(fs::path);
-    const fs::path tdir;
-    NArray<T,N_SLICE,N_FLIP>   *pTPSliceFlip;
-    NArray<T,N_SLICE,N_TWIST>  *pTPSliceTwist;
-    NArray<T,N_EDGE4,N_EDGE8>  *pTPEdge4Edge8;
-    NArray<T,N_EDGE4,N_CORNER> *pTPEdge4Corner;
-};
-template<typename T=mt_value_t>
-using SingletonTM = Singleton<TableMove<T>>;
-template<typename T=pt_value_t>
-using SingletonTP = Singleton<TablePrunning<T>>;
-inline const auto &get_TM() { return SingletonTM<>::instance(); }
-inline const auto &get_TP() { return SingletonTP<>::instance(); }
-inline bool tables_ready(fs::path dir = {})
-{
-    return TableMove<>::is_ready(dir) && TablePrunning<>::is_ready(dir);
-}
-inline void preload_tables()
-{
-    (void) get_TM();
-    (void) get_TP();
-}
-}
-#if defined(VERBOSE) && VERBOSE
-#define VPRINT(...) printf(__VA_ARGS__)
-#else
-#define VPRINT(...)
-#endif
-namespace cube::pdb {
-using namespace cube::data;
-auto table_dir_or_default(fs::path dir) -> fs::path
-{
-    return dir.empty() ? cube::config::get_table_dir() : dir;
-}
-template <typename Table>
-void save_to(const Table &table, fs::path path)
-{
-    std::ofstream f(path, std::ios::binary);
-    if(f.is_open()) {
-        f.write(reinterpret_cast<const char*>(&table.data), sizeof(table.data));
-        f.close();
-    }
-}
-template <typename Table>
-void load_from(Table &table, fs::path path)
-{
-    VPRINT("loading table from %s...", path.c_str());
-    std::ifstream f(path,std::ios::binary);
-    f.read(reinterpret_cast<char*>(&table.data), sizeof(table.data));
-    f.close();
-    VPRINT("done.\n");
-}
-template<typename T>
-template<typename Table, typename F1, typename F2>
-std::enable_if_t<Table::shape[0] == N_MOVE>
-TableMove<T>::buildMoveTable(Table &t, F1&& coord2i, F2&& i2coord, fs::path filename)
-{
-    VPRINT("creating move table %s of shape (%zu,%zu)... ",
-           filename.c_str(), t.shape[0], t.shape[1]);
-    for(size_t i = 0; i < t.shape[0]; i++) for(size_t j = 0; j < t.shape[1]; j++) {
-        t[i][j] = coord2i(i2coord(j) * ElementaryMove[i]);
-    }
-    if(filename != "") save_to(t, tdir/filename);
-    VPRINT("done.\n");
-}
-template<typename T>
-TableMove<T>::TableMove(fs::path dir)
-:tdir{table_dir_or_default(dir)}
-{
-    VPRINT("INIT MOVE TABLES -- \n");
-    pTMTwist     = new NArray<T,N_MOVE,N_TWIST>;
-    pTMFlip      = new NArray<T,N_MOVE,N_FLIP>;
-    pTMSlice     = new NArray<T,N_MOVE,N_SLICE>;
-    pTMCorner    = new NArray<T,N_MOVE,N_CORNER>;
-    pTMEdge4     = new NArray<T,N_MOVE,N_EDGE4>;
-    pTMEdge8     = new NArray<T,N_MOVE,N_EDGE8>;
-    if(!is_ready(tdir)) {
-        if(!fs::exists(tdir)) fs::create_directories(tdir);
-        buildMoveTable(*pTMTwist, Coord::co2twist, Coord::twist2co, "tm_twist.dat");
-        buildMoveTable(*pTMFlip, Coord::eo2flip, Coord::flip2eo, "tm_flip.dat");
-        buildMoveTable(*pTMSlice, Coord::ep2slice, Coord::slice2ep, "tm_slice.dat");
-        buildMoveTable(*pTMCorner, Coord::cp2corner, Coord::corner2cp, "tm_corner.dat");
-        buildMoveTable(*pTMEdge4, Coord::ep2edge4, Coord::edge42ep, "tm_edge4.dat");
-        buildMoveTable(*pTMEdge8, Coord::ep2edge8, Coord::edge82ep, "tm_edge8.dat");
-    } else {
-        load_from(*pTMTwist, tdir/"tm_twist.dat");
-        load_from(*pTMFlip, tdir/"tm_flip.dat");
-        load_from(*pTMSlice, tdir/"tm_slice.dat");
-        load_from(*pTMCorner, tdir/"tm_corner.dat");
-        load_from(*pTMEdge4, tdir/"tm_edge4.dat");
-        load_from(*pTMEdge8, tdir/"tm_edge8.dat");
-    }
-    VPRINT("-- DONE.\n");
-}
-template<typename T>
-TableMove<T>::~TableMove()
-{
-    delete pTMTwist;
-    delete pTMFlip;
-    delete pTMSlice;
-    delete pTMCorner;
-    delete pTMEdge4;
-    delete pTMEdge8;
-}
-template<typename T>
-bool TableMove<T>::is_ready(fs::path tdir)
-{
-    tdir = table_dir_or_default(tdir);
-    return fs::exists(tdir/"tm_twist.dat") &&
-           fs::exists(tdir/"tm_flip.dat") &&
-           fs::exists(tdir/"tm_slice.dat") &&
-           fs::exists(tdir/"tm_corner.dat") &&
-           fs::exists(tdir/"tm_edge4.dat") &&
-           fs::exists(tdir/"tm_edge8.dat");
-}
-template<typename T>
-template<typename Table, typename MT1, typename MT2>
-std::enable_if_t<Table::shape[0] == MT1::shape[1] && Table::shape[1] == MT2::shape[1]>
-TablePrunning<T>::buildPrunningTable(
-    Table &t, const MT1 &mt1, const MT2 &mt2, fs::path filename)
-{
-    VPRINT("creating prunning table %s of shape (%zu,%zu):\n",
-           filename.c_str(), mt1.shape[1], mt2.shape[1]);
-    std::fill_n(&t.data[0][0], t.size, (typename Table::value_type) ~0UL);
-    t[0][0] = 0;
-    typename Table::value_type depth = 0;
-    size_t count = 1;
-    VPRINT("\tdepth %2d: %10zu / %-10zu.\n", depth, count, t.size);
-    while(count < t.size)
-    {
-        for(size_t i = 0; i < t.shape[0]; i++) for(size_t j = 0; j < t.shape[1]; j++)
-        if(t[i][j] == depth) {
-            for(auto k = 0; k < N_MOVE; k++) {
-                auto ii = mt1[k][i], jj = mt2[k][j];
-                if(t[ii][jj] == (typename Table::value_type)~0UL) { t[ii][jj] = depth + 1; count++; }
-            }
-        }
-        depth++;
-        VPRINT("\tdepth %2d: %10zu / %-10zu.\n", depth, count, t.size);
-    }
-    if(filename != "") save_to(t, tdir/filename);
-    VPRINT("done.\n");
-}
-template<typename T>
-TablePrunning<T>::TablePrunning(fs::path dir)
-:tdir{table_dir_or_default(dir)}
-{
-    VPRINT("INIT PRUNNING TABLES -- \n");
-    pTPSliceFlip     = new NArray<T,N_SLICE,N_FLIP>;
-    pTPSliceTwist    = new NArray<T,N_SLICE,N_TWIST>;
-    pTPEdge4Edge8    = new NArray<T,N_EDGE4,N_EDGE8>;
-    pTPEdge4Corner   = new NArray<T,N_EDGE4,N_CORNER>;
-    if(!is_ready(tdir)) {
-        const auto &TM = get_TM();
-        buildPrunningTable(*pTPSliceTwist, *TM.pTMSlice, *TM.pTMTwist, "tp_slicetwist.dat");
-        buildPrunningTable(*pTPSliceFlip, *TM.pTMSlice, *TM.pTMFlip, "tp_sliceflip.dat");
-        buildPrunningTable(*pTPEdge4Corner, *TM.pTMEdge4, *TM.pTMCorner, "tp_edge4corner.dat");
-        buildPrunningTable(*pTPEdge4Edge8, *TM.pTMEdge4, *TM.pTMEdge8, "tp_edge4edge8.dat");
-    } else {
-        load_from(*pTPSliceTwist, tdir/"tp_slicetwist.dat");
-        load_from(*pTPSliceFlip, tdir/"tp_sliceflip.dat");
-        load_from(*pTPEdge4Corner,tdir/"tp_edge4corner.dat");
-        load_from(*pTPEdge4Edge8, tdir/"tp_edge4edge8.dat");
-    }
-    VPRINT("-- DONE.\n");
-}
-template<typename T>
-TablePrunning<T>::~TablePrunning()
-{
-    delete pTPSliceFlip;
-    delete pTPSliceTwist;
-    delete pTPEdge4Edge8;
-    delete pTPEdge4Corner;
-}
-template<typename T>
-bool TablePrunning<T>::is_ready(fs::path tdir)
-{
-    tdir = table_dir_or_default(tdir);
-    return fs::exists(tdir/"tp_slicetwist.dat") &&
-           fs::exists(tdir/"tp_sliceflip.dat") &&
-           fs::exists(tdir/"tp_edge4corner.dat") &&
-           fs::exists(tdir/"tp_edge4edge8.dat");
-}
-template struct TableMove<>;
-template struct TablePrunning<>;
-}
-namespace cube::solver {
-using namespace cube::data;
-using namespace cube::pdb;
-enum enum_phase { Ph1=0, Ph2=1 };
-struct solve_session
-{
-    const Coord root;
-    const size_t good_enough;
-    const size_t max_step;
-};
-class TwoPhaseSolver
-{
-public:
-    auto solve(const Coord &c, int max_step = DS)
-        -> std::tuple<bool,std::vector<TurnMove>,std::vector<TurnMove>>;
-protected:
-    template<enum_phase PhX>
-    bool search_phase(const Coord &c, size_t togo);
-    template<enum_phase PhX>
-    static Coord transform(const Coord &c, const TurnMove &m);
-    template<enum_phase PhX>
-    static size_t distance(const Coord &c);
-    static constexpr int D0 = 12, D1 = 18, DS = D0+D1;
-    template<enum_phase PhX> static constexpr auto& D = std::get<PhX>(std::tie(D0,D1));
-    static constexpr std::array<TurnMove,18> EM0
-        = { Ux1,Ux2,Ux3,Rx1,Rx2,Rx3,Fx1,Fx2,Fx3,Dx1,Dx2,Dx3,Lx1,Lx2,Lx3,Bx1,Bx2,Bx3 };
-    static constexpr std::array<TurnMove,10> EM1
-        = { Ux1,Ux2,Ux3,Rx2,Fx2,Dx1,Dx2,Dx3,Lx2,Bx2 };
-    template<enum_phase PhX>
-    static constexpr auto& EM = std::get<PhX>(std::tie(EM0,EM1));
-protected:
-    template<enum_phase PhX>
-    void reset_ph_sofar() { sofar_[PhX].fill(-1); }
-    template<enum_phase PhX>
-    void set_ph_sofar(int step, int move) { sofar_[PhX][step] = move; }
-    template<enum_phase PhX>
-    int get_ph_sofar(int step) { return sofar_[PhX][step]; }
-    template<enum_phase PhX>
-    void set_ph_solution(size_t L)
-    {
-        rsolution_[PhX].first = L;
-        std::copy(sofar_[PhX].begin(), sofar_[PhX].begin()+L, rsolution_[PhX].second.begin());
-    }
-    template<enum_phase PhX>
-    auto get_ph_solution() const -> std::vector<TurnMove>
-    {
-        size_t n = rsolution_[PhX].first;
-        if(n == 0) return {};
-        std::vector<TurnMove> sol(n);
-        for(auto i = 0; i < n; i++) {
-            sol[i] = static_cast<TurnMove>(rsolution_[PhX].second[n-1-i]);
-        }
-        return sol;
-    }
-    Coord ph2_origin(Coord c) const;
-private:
-    std::array<std::array<int,DS+2>,2>                  sofar_;
-    std::array<std::pair<size_t,std::array<int,DS>>,2>  rsolution_;
-};
-class TwoPhaseOptimSolver: public TwoPhaseSolver
-{
-public:
-    auto solve(const Coord &c, size_t good = 0)
-        -> std::tuple<bool,std::vector<TurnMove>,std::vector<TurnMove>>;
-    int start_search_ph1(const Coord& c, size_t d, size_t d_orig, size_t maxL);
-    bool start_search_ph2(const Coord& c, size_t togo);
-private:
-    Coord root_;
-    size_t good_;
-};
-}
-namespace cube::solver {
-#define TM cube::pdb::get_TM()
-#define TP cube::pdb::get_TP()
-inline bool is_dull_pair(const TurnMove A, const int B)
-{
-    if(!(B >= Ux1 && B <= Bx3)) return false;
-    return (A/3 == B/3) || (A/3 == B/3+3);
-}
-template<enum_phase I>
-Coord TwoPhaseSolver::transform(const Coord &c, const TurnMove &m)
-{
-    if constexpr (I == Ph1)
-    return Coord {
-        (*TM.pTMTwist)[m][c.twist], (*TM.pTMFlip)[m][c.flip], (*TM.pTMSlice)[m][c.slice],
-        -1,-1,-1
-    };
-    else
-    return Coord {
-        0,0,0,
-        (*TM.pTMCorner)[m][c.corner], (*TM.pTMEdge4)[m][c.edge4], (*TM.pTMEdge8)[m][c.edge8]
-    };
-}
-template<enum_phase I>
-size_t TwoPhaseSolver::distance(const Coord &c)
-{
-    if constexpr (I == Ph1)
-    return std::max((*TP.pTPSliceTwist)[c.slice][c.twist],
-                    (*TP.pTPSliceFlip)[c.slice][c.flip]);
-    else
-    return std::max((*TP.pTPEdge4Corner)[c.edge4][c.corner],
-                    (*TP.pTPEdge4Edge8)[c.edge4][c.edge8]);
-}
-template<enum_phase PhX>
-bool TwoPhaseSolver::search_phase(const Coord &c, size_t togo)
-{
-    if(togo < distance<PhX>(c)) return false;
-    if(togo == 0) return distance<PhX>(c) == 0;
-    for(auto m: EM<PhX>)
-    {
-        if(is_dull_pair(m,get_ph_sofar<PhX>(togo))) continue;
-        set_ph_sofar<PhX>(togo-1,m);
-        bool ret = search_phase<PhX>(transform<PhX>(c,m), togo-1);
-        if(ret) return true;
-    }
-    return false;
-}
-Coord TwoPhaseSolver::ph2_origin(Coord c) const
-{
-    int corner = c.corner, edge4, edge8;
-    auto ep = Coord::see2ep(c.slice,c.edge4,c.edge8);
-    for(auto &m : get_ph_solution<Ph1>())
-    {
-        corner = (*TM.pTMCorner)[m][corner];
-        ep = ep * ElementaryMove[m].ep;
-    }
-    edge4 = Coord::ep2edge4(ep);
-    edge8 = Coord::ep2edge8(ep);
-    return Coord { 0,0,0,corner,edge4,edge8 };
-}
-auto TwoPhaseSolver::solve(const Coord &c, int step)
-    -> std::tuple<bool,std::vector<TurnMove>,std::vector<TurnMove>>
-{
-    reset_ph_sofar<Ph1>();
-    reset_ph_sofar<Ph2>();
-    const size_t maxL = std::clamp(step,0,DS);
-    for(auto d1 = distance<Ph1>(c); d1 <= maxL; d1++)
-    {
-        bool ret1 = search_phase<Ph1>(c,d1);
-        if(!ret1) continue;
-        set_ph_solution<Ph1>(d1);
-        auto c2 = ph2_origin(c);
-        for(auto d2 = distance<Ph2>(c2); d2 <= maxL-d1; d2++)
-        {
-            bool ret2 = search_phase<Ph2>(c2,d2);
-            if(!ret2) continue;
-            set_ph_solution<Ph2>(d2);
-            return {true, get_ph_solution<Ph1>(), get_ph_solution<Ph2>()};
-        }
-    }
-    return {false, {}, {}};
-}
-auto TwoPhaseOptimSolver::solve(const Coord &c, size_t good)
-    -> std::tuple<bool,std::vector<TurnMove>,std::vector<TurnMove>>
-{
-    good_ = good;
-    root_ = c;
-    size_t bestL = DS + 1;
-    reset_ph_sofar<Ph1>();
-    for(auto togo = distance<Ph1>(c); togo < bestL; ++togo)
-    {
-        int l = start_search_ph1(c,togo,togo,bestL-1);
-        if(l >= 0) bestL = std::min(bestL,static_cast<size_t>(l));
-        if(bestL <= good_) break;
-    }
-    if(bestL > DS) return {false, {}, {}};
-    else return { true, get_ph_solution<Ph1>(), get_ph_solution<Ph2>() };
-}
-int TwoPhaseOptimSolver::start_search_ph1(const Coord& c, size_t d, size_t d_orig, size_t maxL)
-{
-    if(d < distance<Ph1>(c)) return -1;
-    if(d == 0) {
-        set_ph_solution<Ph1>(d_orig);
-        auto c2 = ph2_origin(root_);
-        reset_ph_sofar<Ph2>();
-        for(auto togo = distance<Ph2>(c2); togo <= maxL; ++togo)
-        {
-            bool succ = start_search_ph2(c2,togo);
-            if(succ) {
-                set_ph_solution<Ph2>(togo);
-                return togo;
-            }
-        }
-        return -1;
-    }
-    int bestL = maxL + 1;
-    for(auto m: EM<Ph1>)
-    {
-        if(is_dull_pair(m, get_ph_sofar<Ph1>(d))) continue;
-        set_ph_sofar<Ph1>(d-1,m);
-        auto l = start_search_ph1(transform<Ph1>(c,m), d-1, d_orig, bestL-2);
-        if(l >= 0 && bestL > l+1) {
-            bestL = l+1;
-        }
-        if(bestL + d_orig - d <= good_) break;
-        if(d >= bestL) break;
-    }
-    return bestL > static_cast<int>(maxL) ? -1 : bestL;
-}
-bool TwoPhaseOptimSolver::start_search_ph2(const Coord& c, size_t togo)
-{   if(togo < distance<Ph2>(c)) return false;
-    if(togo == 0) return distance<Ph2>(c) == 0;
-    for(auto m: EM<Ph2>)
-    {
-        if(is_dull_pair(m, get_ph_sofar<Ph2>(togo))) continue;
-        set_ph_sofar<Ph2>(togo-1,m);
-        bool ret = start_search_ph2(transform<Ph2>(c,m), togo-1);
-        if(ret) return true;
-    }
-    return false;
-}
-}
-namespace cube::utils {
-inline auto get_cache_dir() -> std::filesystem::path
-{
-#ifdef _WIN32
-    const char* localAppData = std::getenv("LOCALAPPDATA");
-    if (localAppData) return std::filesystem::path(localAppData);
-#elif __APPLE__
-    const char* home = std::getenv("HOME");
-    if (home) return std::filesystem::path(home) / "Library" / "Caches";
-#else
-    const char* xdgCache = std::getenv("XDG_CACHE_HOME");
-    if (xdgCache) return std::filesystem::path(xdgCache);
-    const char* home = std::getenv("HOME");
-    if (home) return std::filesystem::path(home) / ".cache";
-#endif
-    throw std::runtime_error("Unable to determine cache path");
-}
-template<typename F, typename... Args>
-inline auto time_execution(F&& f, Args&&... args)
-{
-    using Rf = std::invoke_result_t<F, Args...>;
-    auto start = std::chrono::high_resolution_clock::now();
-    auto duration_from_start = [start](){
-        auto end = std::chrono::high_resolution_clock::now();
-        return std::chrono::duration_cast<std::chrono::microseconds>(end-start);
-    };
-    if constexpr (std::is_void_v<Rf>) {
-        std::forward<F>(f)(std::forward<Args>(args)...);
-        return std::make_pair(
-            duration_from_start(),
-            std::nullopt
-        );
-    } else {
-        auto result = std::forward<F>(f)(std::forward<Args>(args)...);
-        return std::make_pair(
-            duration_from_start(),
-            std::make_optional(result)
-        );
-    }
-}
-}
-namespace cube::config
-{
-namespace fs = std::filesystem;
-static fs::path& get_custom_dir_storage()
-{
-    static fs::path custom_dir = "";
-    return custom_dir;
-}
-void set_table_dir(fs::path dir)
-{
-    get_custom_dir_storage() = dir;
-}
-fs::path get_table_dir()
-{
-    const auto user_dir = get_custom_dir_storage();
-    if(!user_dir.empty()) return user_dir;
-    if(const char *env = std::getenv("CUBE_TABLE_DIR")) {
-        if(env[0] != '\0') return fs::path(env);
-    }
-    try {
-        return utils::get_cache_dir() / "cube.coshz" / "tables";
-    } catch (...) {
-        return fs::absolute("tables");
-    }
-}
-}
-namespace cube::internal
-{
-using namespace cube::data;
-std::string apply_maneuver(std::string_view s, const std::vector<TurnMove>& ms);
-auto parse_maneuver(std::string_view s) -> std::vector<TurnMove>;
-inline auto operator""_Tm(const char* ts, size_t n) -> std::vector<TurnMove>
-{
-    return parse_maneuver(std::string(ts,n));
-}
-bool is_valid_maneuver(std::string_view);
-}
-namespace cube {
-using namespace cube::data;
-namespace internal {
-std::string apply_maneuver(std::string_view s, const std::vector<TurnMove> &ms)
-{
-    assert(s.size() == 54 && "invalid cube length");
-    char buf[2][54];
-    std::memcpy(buf[0], s.data(), 54);
-    size_t curr = 0;
-    for(const auto &m : ms)
-    {
-        const auto& p   = ElementaryPerm[m].f;
-        const char* src = buf[curr];
-        char*       dst = buf[1-curr];
-        for(int i = 0; i < 54; ++i) dst[i] = src[p[i]];
-        curr = 1-curr;
-    }
-    return std::string(buf[curr],54);
-}
-auto parse_maneuver(std::string_view s) -> std::vector<TurnMove>
-{
-    assert(is_valid_maneuver(s) && "invalid maneuver");
-    auto expand = [](std::string_view in) -> std::string {
-        static const std::regex group_re(R"(\(([^)]+)\)(?:\{(\d+)\})?)");
-        std::string res;
-        auto start = in.cbegin();
-        std::match_results<std::string_view::const_iterator> m;
-        while (std::regex_search(start, in.cend(), m, group_re)) {
-            res.append(start, m[0].first);
-            int repeat = m[2].matched ? std::stoi(m[2].str()) : 1;
-            for (int i = 0; i < repeat; ++i) res += m[1].str();
-            start = m[0].second;
-        }
-        res.append(start, in.cend());
-        return res;
-    };
-    auto char_to_move = [](char c) -> TurnMove {
-        switch(c) {
-        case 'U': return Ux1;
-        case 'R': return Rx1;
-        case 'F': return Fx1;
-        case 'D': return Dx1;
-        case 'L': return Lx1;
-        case 'B': return Bx1;
-        default: throw std::invalid_argument("char_to_move: ???");
-        }
-    };
-    std::string in = expand(s);
-    std::vector<TurnMove> ms;
-    ms.reserve(in.size());
-    for (char c : in)
-    {
-        switch (c) {
-        case ' ': break;
-        case '2':
-            ms.back() = static_cast<TurnMove>(ms.back() + 1);
-            break;
-        case '\'':
-            ms.back() = static_cast<TurnMove>(ms.back() + 2);
-            break;
-        default:
-            ms.push_back(char_to_move(c));
-            break;
-        }
-    }
-    return ms;
-}
-bool is_valid_maneuver(std::string_view s)
-{
-    static const std::regex pat(
-        R"(\s*(([UDLRFB]['23]?|\(([UDLRFB]['23]?\s*)+\)(\{\d+\})?)\s*)*)"
-    );
-    return std::regex_match(s.begin(), s.end(), pat);
-}
-}
-}
-#ifndef CUBE_EXPORT_H
-#define CUBE_EXPORT_H
-#ifdef CUBE_STATIC_DEFINE
-#  define CUBE_EXPORT
-#  define CUBE_NO_EXPORT
-#else
-#  ifndef CUBE_EXPORT
-#    ifdef cube_shared_EXPORTS
-#      define CUBE_EXPORT __attribute__((visibility("default")))
-#    else
-#      define CUBE_EXPORT __attribute__((visibility("default")))
-#    endif
-#  endif
-#  ifndef CUBE_NO_EXPORT
-#    define CUBE_NO_EXPORT __attribute__((visibility("hidden")))
-#  endif
-#endif
-#ifndef CUBE_DEPRECATED
-#  define CUBE_DEPRECATED __attribute__ ((__deprecated__))
-#endif
-#ifndef CUBE_DEPRECATED_EXPORT
-#  define CUBE_DEPRECATED_EXPORT CUBE_EXPORT CUBE_DEPRECATED
-#endif
-#ifndef CUBE_DEPRECATED_NO_EXPORT
-#  define CUBE_DEPRECATED_NO_EXPORT CUBE_NO_EXPORT CUBE_DEPRECATED
-#endif
-#if 0
-#  ifndef CUBE_NO_DEPRECATED
-#    define CUBE_NO_DEPRECATED
-#  endif
-#endif
-#endif
-#ifndef __cplusplus
-#error "This is a C++ header; please use a C++ compiler OR include cube/cube.h instead"
-#endif
-namespace cube {
-constexpr std::string_view cubeId = "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB";
-enum class SolveResult : int32_t {
-    Success       = 0,
-    Unsolvable    = 1,
-    NotFound      = 2,
-    InvalidSrc    = 3,
-    InvalidTgt    = 4,
-    UnknownErr    = 5
-};
-CUBE_EXPORT constexpr std::string_view to_string(SolveResult status) noexcept {
-    switch (status) {
-    case SolveResult::Success:      return "Success";
-    case SolveResult::Unsolvable:   return "The cube configuration is unsolvable";
-    case SolveResult::NotFound:     return "No solution found within the step limit";
-    case SolveResult::InvalidSrc:   return "Invalid source color configuration";
-    case SolveResult::InvalidTgt:   return "Invalid target color configuration";
-    case SolveResult::UnknownErr:   return "Unknown error";
-    }
-}
-enum class PermFormat : int {
-    Face  = 0,
-    Cubie = 1,
-    Cycle = 2
-};
-struct Solution {
-    SolveResult status;
-    std::string maneuver;
-    bool is_success() const noexcept
-    {
-        return status == SolveResult::Success;
-    }
-};
-CUBE_EXPORT bool is_solvable(std::string_view color_cube);
-[[nodiscard]] CUBE_EXPORT Solution solve(
-    std::string_view src,
-    std::string_view tgt = cubeId,
-    int good = 22
-);
-[[nodiscard]] CUBE_EXPORT std::string apply_maneuver(
-    std::string_view maneuver,
-    std::string_view cube = cubeId
-);
-[[nodiscard]] CUBE_EXPORT std::string show_permutation(
-    std::string_view ms_or_cube,
-    PermFormat format = PermFormat::Cycle
-);
-}
-namespace cube {
-CUBE_EXPORT void set_table_dir(std::filesystem::path dir);
-CUBE_EXPORT auto get_table_dir() -> std::filesystem::path;
-CUBE_EXPORT bool tables_ready();
-CUBE_EXPORT void preload_tables();
-CUBE_EXPORT bool is_valid_config(std::string_view cube);
-CUBE_EXPORT bool is_valid_maneuver(std::string_view maneuver);
-}
-namespace cube::show {
-using std::size_t;
-using namespace cube::data;
-template<size_t N>
-using StringArr = std::array<std::string_view,N>;
-inline constexpr StringArr< 8> CornerToString       = { "urf", "ufl", "ulb", "ubr", "dfr", "dlf", "dbl", "drb" };
-inline constexpr StringArr<12> EdgeToString         = { "ur","uf","ul","ub","dr","df","dl","db","fr","fl","bl","br" };
-inline constexpr StringArr< 6> CenterToString       = { "u", "r", "f", "d", "l", "b" };
-inline constexpr StringArr< 3> OrientationToString  = { "", "+", "-" };
-inline constexpr StringArr<18> Move2Str             = { "U","U2","U'","R","R2","R'","F","F2","F'","D","D2","D'","L","L2","L'","B","B2","B'" };
-inline constexpr StringArr<54> Face2Str             = { "U1","U2","U3","U4","U5","U6","U7","U8","U9","R1","R2","R3","R4","R5","R6","R7","R8","R9","F1","F2","F3","F4","F5","F6","F7","F8","F9","D1","D2","D3","D4","D5","D6","D7","D8","D9","L1","L2","L3","L4","L5","L6","L7","L8","L9","B1","B2","B3","B4","B5","B6","B7","B8","B9" };
-inline constexpr std::string_view ColorSet          = "URFDLB";
-inline constexpr std::string_view CornerSet         = "ABCDEFGH";
-inline constexpr std::string_view EdgeSet           = "opqrstuvwxyz";
-enum class CubeFormat {
-    Face,
-    Cubie,
-    Cycle
-};
-template<typename Array, typename Formatter>
-inline std::string seq2str_fmt(const Array &xs,
-                           Formatter && fmt,
-                           std::string_view sep="",
-                           std::string_view pre="",
-                           std::string_view suf="")
-{
-    const size_t len = xs.size();
-    if(len == 0) { return std::string(pre) + std::string(suf); }
-    std::stringstream ss;
-    ss << pre << fmt(xs[0]);
-    for(size_t i = 1; i < len; ++i) ss << sep << fmt(xs[i]);
-    ss << suf;
-    return ss.str();
-}
-template<typename Container>
-inline std::string seq2str(const Container &xs,
-                           std::string_view sep="",
-                           std::string_view pre="",
-                           std::string_view suf="")
-{
-    auto default_formatter = [](const auto& val) {
-        using T = std::decay_t<decltype(val)>;
-        if constexpr (std::is_same_v<T,uint8_t> || std::is_same_v<T,int8_t>) {
-            return static_cast<int>(val);
-        } else {
-            return val;
-        }
-    };
-    return seq2str_fmt(xs, default_formatter, sep, pre, suf);
-}
-inline std::string to_string(const ColorState& cs)
-{
-    return seq2str_fmt(cs.s, [](auto v) {
-        switch(v){
-        case U: return 'U';
-        case R: return 'R';
-        case F: return 'F';
-        case D: return 'D';
-        case L: return 'L';
-        case B: return 'B';
-        default: throw std::invalid_argument("to_string(ColorState): ???");
-        }
-    });
-}
-inline std::string to_string(const FaceCube& fc, bool use_digit=true)
-{
-    if(use_digit){
-        return seq2str(fc.f);
-    } else {
-        return seq2str_fmt(fc.f, [](auto v) { return Face2Str[v]; });
-    }
-}
-inline std::string to_string(const CubieCube& cc, CubeFormat fmt)
-{
-    switch(fmt){
-    case CubeFormat::Face:
-        return to_string(cc.toFaceCube(), false);
-    case CubeFormat::Cubie:
-        return seq2str_fmt(cc.cp, [](auto v) { return CornerSet[v]; }) +
-               seq2str_fmt(cc.co, [](auto v) { return static_cast<char>('0'+v); }) +
-               seq2str_fmt(cc.ep, [](auto v) { return EdgeSet[v]; }) +
-               seq2str_fmt(cc.eo, [](auto v) { return static_cast<char>('0'+v);});
-    default:
-        const auto [fixed_corner,cycles_corner] = decomposite(cc.cp);
-        const auto [fixed_edge, cycles_edge ]   = decomposite(cc.ep);
-        std::string s1, s2;
-        s1.reserve(64);
-        s2.reserve(64);
-        auto append_fixed = [&](std::string& out, const auto& fixed, const auto& ori_arr, auto name_table) {
-            for(auto idx: fixed) {
-                if(ori_arr[idx] == 0) continue;
-                out += '(';
-                out += OrientationToString[ori_arr[idx]];
-                out += name_table[idx];
-                out += ')';
-            }
-        };
-        auto append_cycles = [&](std::string& out, const auto& cycles, const auto& ori_arr, auto name_table) {
-            for(const auto& cycle: cycles) {
-                const size_t N = cycle.size();
-                if(N == 0) continue;
-                out += '(';
-                for(int i = 0; i < N; i++) {
-                    if(i > 0) out += ',';
-                    size_t prev_idx = cycle[(N-1+i)%N];
-                    out += OrientationToString[ori_arr[prev_idx]];
-                    out += name_table[cycle[i]];
-                }
-                out += ')';
-            }
-        };
-        append_fixed(s1, fixed_corner, cc.co, CornerToString);
-        append_cycles(s1, cycles_corner, cc.co, CornerToString);
-        append_fixed(s2, fixed_edge, cc.eo, EdgeToString);
-        append_cycles(s2, cycles_edge, cc.eo, EdgeToString);
-        return s1.empty() && s2.empty() ? "id" : s1 + s2;
-    }
-}
-inline std::string to_string(const std::vector<TurnMove>& ms)
-{
-    return seq2str_fmt(ms, [](auto v){
-        return Move2Str[v];
-    }, " ");
-}
-}
-namespace cube {
-using namespace cube::data;
-namespace fs = std::filesystem;
-void set_table_dir(fs::path dir) { config::set_table_dir(dir); }
-auto get_table_dir() -> fs::path { return config::get_table_dir(); }
-bool tables_ready() { return pdb::tables_ready(); }
-void preload_tables() { pdb::preload_tables(); }
-bool is_valid_config(std::string_view cube) { return ColorState::is_valid_config(cube); }
-bool is_valid_maneuver(std::string_view maneuver) { return internal::is_valid_maneuver(maneuver); }
-cube::solver::TwoPhaseOptimSolver TPS;
-Solution solve(std::string_view src, std::string_view tgt, int good)
-{
-    if (src != cubeId && !is_valid_config(src)) return {SolveResult::InvalidSrc, ""};
-    if (tgt != cubeId && !is_valid_config(tgt)) return {SolveResult::InvalidTgt, ""};
-    if (src == tgt) return {SolveResult::Success, ""};
-    auto cc_src = ColorState::fromString(src).toCubieCube();
-    auto cc_tgt = ColorState::fromString(tgt).toCubieCube();
-    CubieCube cc = ~cc_tgt * cc_src;
-    if (!cc.isSolvable()) return {SolveResult::Unsolvable, ""};
-    const auto& [found, s1, s2] = TPS.solve(pdb::Coord::CubieCube2Coord(cc), good < 0 ? 0 : good);
-    if (!found) return {SolveResult::NotFound, ""};
-    std::vector<TurnMove> sol;
-    size_t n1 = s1.size(), n2 = s2.size();
-    if (!s1.empty() && !s2.empty() && s1[n1 - 1] / 3 == s2[0] / 3) {
-        sol.insert(sol.end(), s1.begin(), s1.end() - 1);
-        int m = (s1[n1 - 1] + s2[0] - s2[0] / 3 * 6 + 2) % 4;
-        if (m != 0) sol.push_back(static_cast<TurnMove>(s2[0] / 3 * 3 + m - 1));
-        sol.insert(sol.end(), s2.begin() + 1, s2.end());
-    } else {
-        sol.insert(sol.end(), s1.begin(), s1.end());
-        sol.insert(sol.end(), s2.begin(), s2.end());
-    }
-    return {SolveResult::Success, show::to_string(sol)};
-}
-bool is_solvable(std::string_view color_cube) {
-    return is_valid_config(color_cube) &&
-           ColorState::fromString(color_cube).toCubieCube().isSolvable();
-}
-std::string apply_maneuver(std::string_view maneuver, std::string_view cube_state) {
-    if (cube_state.size() != 54 || !is_valid_maneuver(maneuver)) {
-        throw std::invalid_argument("Invalid cube format or maneuver string.");
-    }
-    const auto ms = internal::parse_maneuver(maneuver);
-    return internal::apply_maneuver(cube_state, ms);
-}
-std::string show_permutation(std::string_view ms_or_cube, PermFormat format) {
-    std::string cube_str;
-    if (is_valid_config(ms_or_cube)) {
-        cube_str = ms_or_cube;
-    } else if (is_valid_maneuver(ms_or_cube)) {
-        auto ms = internal::parse_maneuver(ms_or_cube);
-        cube_str = internal::apply_maneuver(cubeId, ms);
-    } else {
-        throw std::invalid_argument("Input is neither a valid configuration nor a valid maneuver.");
-    }
-    auto cc = ColorState::fromString(cube_str).toCubieCube();
-    return show::to_string(cc, show::CubeFormat(static_cast<int>(format)));
-}
-}
-#define CUBE_VERSION_FULL  v0.5.0
-#define CUBE_VERSION_MAJOR 0
-#define CUBE_VERSION_MINOR 5
-#define CUBE_VERSION_PATCH 0
-#define CUBE_VERSION_BUILD
-#ifdef __cplusplus
-    #define Default(x) = x
-#else
-    #define Default(x)
-#endif
-#define CUBE_BS 128
-#define CUBE_ID "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"
-#ifndef CF_ENUM
-    #if defined(__cplusplus)
-        #define CF_ENUM(_type, _name) \
-            int __CF_ENUM_##_name; \
-            enum _name : _type
-    #elif defined(__clang__) || defined(__OBJC__)
-        #define CF_ENUM(_type, _name) \
-            enum _name : _type _name; \
-            enum _name: _type
-    #else
-        #define CF_ENUM(_type, _name) \
-            _type _name; \
-            enum
-    #endif
-#endif
-typedef CF_ENUM(int32_t,SolveResult) {
-    SolveResultSuccess = 0,
-    SolveResultUnsolvable = 1,
-    SolveResultNotFound = 2,
-    SolveResultInvalidSrc = 3,
-    SolveResultInvalidTgt = 4,
-    SolveResultUnknownErr = 5
-};
-static inline const char* solve_result_info(SolveResult sr)
-{
-    switch(sr) {
-    case SolveResultSuccess:    return "Success.";
-    case SolveResultUnsolvable: return "The cube configuration is unsolvable.";
-    case SolveResultNotFound:   return "No solution found within the step limit.";
-    case SolveResultInvalidSrc: return "Invalid source color configuration.";
-    case SolveResultInvalidTgt: return "Invalid target color configuration.";
-    case SolveResultUnknownErr: return "Unknown error.";
-    default:                    return "???";
-    }
-}
-#ifdef __cplusplus
-extern "C" {
-#endif
-CUBE_EXPORT const char *solve_result_to_string(int32_t sr);
-CUBE_EXPORT SolveResult solve(
-    char*       buf,
-    const char* src         Default(CUBE_ID),
-    const char* tgt         Default(CUBE_ID),
-    int         good        Default(22)
-);
-CUBE_EXPORT bool solvable(
-    const char* color_cube  Default(CUBE_ID)
-);
-CUBE_EXPORT bool facecube(
-    char* buf,
-    const char* maneuver    Default(""),
-    const char *cube        Default(CUBE_ID)
-);
-CUBE_EXPORT bool permutation(
-    char*       buf,
-    const char* ms_or_cube  Default(CUBE_ID),
-    int         format      Default(2)
-);
-#ifdef __cplusplus
-}
-#endif
-extern "C" {
+ * Version: v0.5.1
+ * Date: 2026-11-06
+ * Homepage: https://github.com/coshz/cube
+ * License: MIT
+ *
+ * Copyright (c) 2026 coshz <fsinhx@gmail.com>. All rights reserved.
+ */
+
+#include <cstddef>
+#include <vector>
+#include <array>
+#include <algorithm>
+#include <utility>
+#include <numeric>
+#include <stdexcept>
+#include <initializer_list>
+#include <cassert>
+#include <type_traits>
+#include <cstdint>
+#include <string_view>
+#include <set>
+#include <memory>
+#include <filesystem>
+#include <cstdio>
+#include <limits>
+#include <queue>
+#include <exception>
+#include <fstream>
+#include <ios>
+#include <iostream>
+#include <tuple>
+#include <cstdlib>
+#include <mutex>
+#include <string>
+#include <cstring>
+#include <regex>
+#include <sstream>
+#include <stdbool.h>
+#include <stdint.h>
+
+namespace cube::math {
+using std::size_t;
+template<typename VectorLike>
+auto decomposite(const VectorLike &xs) -> std::pair<
+    std::vector<typename VectorLike::value_type>,
+    std::vector<std::vector<typename VectorLike::value_type>>
+>
+{
+    using T = typename VectorLike::value_type;
+    std::vector<T> fixed;
+    std::vector<std::vector<T>> cycles;
+    std::vector<bool> visited(xs.size(),false);
+    for(int i = 0; i < xs.size(); ++i) {
+        if(visited[i]) continue;
+        std::vector<T> cycle;
+        for(int j = xs[i]; j != i; j = xs[j]) {
+            visited[j] = true;
+            cycle.push_back(static_cast<T>(j));
+        }
+        visited[i] = true;
+        cycle.push_back(static_cast<T>(i));
+        if(cycle.size() > 1) cycles.push_back(cycle);
+        else fixed.push_back(cycle[0]);
+    }
+    std::sort(cycles.begin(), cycles.end(), [](auto &v1, auto &v2){
+        return v1.size() > v2.size();
+    });
+    return std::make_pair(fixed,cycles);
+}
+constexpr size_t factorial(size_t n)
+{
+    if(n>=21) throw std::invalid_argument("factorial(n) overflows for n >= 21");
+    return n == 0 ? 1 : n * factorial(n-1);
+}
+constexpr size_t binomial(size_t n, size_t k)
+{
+    if(n < k)   return 0;
+    if(n == k)  return 1;
+    size_t r = 1, m = std::min(k, n-k);
+    for(size_t i = 1; i <= m; i++) r *= n-i+1;
+    for(size_t i = 1; i <= m; i++) r /= i;
+    return r;
+};
+template<typename T, size_t N>
+size_t orderOf(const std::array<T,N> &xs)
+{
+    size_t m = 1;
+    auto cs = decomposite(xs);
+    for(size_t i = 1; i < cs.size(); i++) { m = std::lcm(m, cs[i].size()); }
+    return m;
+}
+template<typename T, size_t N>
+constexpr size_t rankOf(const std::array<T,N> &xs)
+{
+    size_t r = 0;
+    std::array<bool,N> used {false};
+    for(size_t i = 0; i < N; i++)
+    {
+        int cnt = 0;
+        for(int j=0; j < xs[i]; j++) if(!used[j]) cnt++;
+        r += cnt * factorial(N - i - 1);
+        used[xs[i]] = true;
+    }
+    return r;
+}
+template<typename T, size_t N>
+constexpr std::array<T,N> fromRank(size_t r)
+{
+    std::array<T,N> A{};
+    std::array<bool,N> used {false};
+    for(size_t i = 0; i < N; i++)
+    {
+        size_t f = factorial(N - i - 1);
+        size_t cnt = r / f;
+        r %= f;
+        for(size_t j = 0; j < N; j++)
+        {
+            if(used[j]) continue;
+            if(cnt-- == 0) {
+                A[i] = j;
+                used[j] = true;
+                break;
+            }
+        }
+    }
+    return A;
+}
+template<typename F, typename T, size_t N>
+constexpr std::array<T,N+1> nestList(F &&f, T &&x)
+{
+    std::array<T,N+1> r;
+    r[0] = x;
+    for(auto i = 1; i <= N; i++) r[i] = f(r[i-1]);
+    return r;
+}
+template<size_t Begin, size_t End, typename T, size_t N>
+constexpr auto takeByRange(const std::array<T,N>& xs) -> std::array<T,End-Begin+1>
+{
+    static_assert(Begin <= End && End < N);
+    std::array<T,End-Begin+1> ys;
+    std::copy(xs.begin()+Begin,xs.begin()+End+1,ys.begin());
+    return ys;
+}
+template<size_t B, typename Int, size_t N>
+constexpr auto fromDigits(const std::array<Int,N> &xs) -> size_t
+{
+    auto pow = [](size_t base, size_t exp) -> size_t {
+        size_t prod = 1;
+        for(size_t i = 0; i < exp; i++) prod *= base;
+        return prod;
+    };
+    size_t res = 0;
+    for(size_t i = 0; i < N; i++) res += static_cast<size_t>(xs[i]) * pow(B, N-i-1);
+    return res;
+}
+template<size_t B, size_t N, typename Int>
+constexpr auto toDigits(size_t i) -> std::array<Int,N>
+{
+    auto pow = [](size_t base, size_t exp) -> size_t {
+        size_t prod = 1;
+        for(size_t i = 0; i < exp; i++) prod *= base;
+        return prod;
+    };
+    std::array<Int,N> a {};
+    for(size_t k = 0; k < N; k++) {
+        size_t z = pow(B, N-1-k);
+        a[k] = static_cast<Int>(i / z);
+        i = i % z;
+    }
+    return a;
+}
+template<size_t N, size_t M>
+constexpr auto lexicalOrderFromIndices(const std::array<size_t,M> &X) -> int
+{
+    static_assert(M<=N);
+    int rank = 0;
+    for(size_t i = 0; i < M; i++) rank += binomial(N-1-X[i],M-i);
+    return rank;
+}
+template<size_t N, size_t M>
+constexpr auto lexicalOrderToIndices(int rank) -> std::array<size_t,M>
+{
+    static_assert(M<=N);
+    std::array<size_t,M> X {};
+    size_t n=rank, k=0;
+    for(size_t i = 0; i < M; i++) {
+        while(binomial(N-1-k,M-i) > n || n >= binomial(N-k,M-i)) k++;
+        X[i] = k;
+        n -= binomial(N-1-k,M-i);
+        k += 1;
+    }
+    return X;
+}
+template<size_t N, typename ArrayLike>
+constexpr bool isValidPermutation(const ArrayLike& xs)
+{
+    if(xs.size() != N) return false;
+    std::array<bool,N> visited{};
+    for(auto i = 0; i < N; ++i) {
+        size_t val = xs[i];
+        if(xs[i] < 0 || xs[i] >= N) return false;
+        if(visited[val]) return false;
+        visited[val] = true;
+    }
+    return true;
+}
+template<typename Array, typename Perm>
+constexpr Array backpermute(const Array& src, const Perm& P)
+{
+    Array out{src};
+    for (size_t i = 0; i < P.size(); ++i) { out[i] = src[P[i]]; }
+    return out;
+}
+template<typename Array, typename Perm>
+constexpr Array forepermute(const Array& src, const Perm& P)
+{
+    Array out{src};
+    for (size_t i = 0; i < P.size(); ++i) { out[P[i]] = src[i]; }
+    return out;
+}
+}
+namespace cube::data {
+enum Face       { U1,U2,U3,U4,U5,U6,U7,U8,U9,R1,R2,R3,R4,R5,R6,R7,R8,R9,F1,F2,F3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,D7,D8,D9,L1,L2,L3,L4,L5,L6,L7,L8,L9,B1,B2,B3,B4,B5,B6,B7,B8,B9 };
+enum Layer      { U,R,F,D,L,B };
+enum TurnMove   { Ux1,Ux2,Ux3,Rx1,Rx2,Rx3,Fx1,Fx2,Fx3,Dx1,Dx2,Dx3,Lx1,Lx2,Lx3,Bx1,Bx2,Bx3 };
+enum Symmetry   { S_URF3,S_F2,S_U4,S_LR2 };
+enum ColorIndex { UCol,RCol,FCol,DCol,LCol,BCol,NoCol };
+enum Corner     { URF,UFL,ULB,UBR,DFR,DLF,DBL,DRB };
+enum Edge       { UR,UF,UL,UB,DR,DF,DL,DB,FR,FL,BL,BR };
+struct OrientedCorner   { Corner c; unsigned o; };
+struct OrientedEdge     { Edge e;   unsigned o; };
+typedef ColorIndex      SingleFace[9];
+typedef ColorIndex      CornerColorIndex[8][3];
+typedef ColorIndex      EdgeColorIndex[12][2];
+typedef ColorIndex      FaceletColor[54];
+typedef Face            CentreFacelet[6];
+typedef Face            CornerFacelet[8][3];
+typedef Face            EdgeFacelet[12][2];
+typedef Face            Facelet[54];
+typedef OrientedCorner  CornerCubie[8];
+typedef OrientedEdge    EdgeCubie[12];
+typedef Corner          EdgeNeighbour[12][2];
+const CornerColorIndex  CCI = {{UCol,RCol,FCol},{UCol,FCol,LCol},{UCol,LCol,BCol},{UCol,BCol,RCol},{DCol,FCol,RCol},{DCol,LCol,FCol},{DCol,BCol,LCol},{DCol,RCol,BCol}};
+const EdgeColorIndex    ECI = {{UCol,RCol},{UCol,FCol},{UCol,LCol},{UCol,BCol},{DCol,RCol},{DCol,FCol}, {DCol,LCol},{DCol,BCol},{FCol,RCol},{FCol,LCol},{BCol,LCol},{BCol,RCol}};
+const CentreFacelet     CC  = {U5,R5,F5,D5,L5,B5};
+const CornerFacelet     CF  = {{U9,R1,F3},{U7,F1,L3},{U1,L1,B3},{U3,B1,R3},{D3,F9,R7},{D1,L9,F7},{D7,B9,L7},{D9,R9,B7}};
+const EdgeFacelet       EF  = {{U6,R2},{U8,F2},{U4,L2},{U2,B2},{D6,R8},{D2,F8}, {D4,L8},{D8,B8},{F6,R4},{F4,L6},{B6,L4},{B4,R6}};
+const EdgeNeighbour     EN  = {{URF,UBR},{UFL,URF},{ULB,UFL},{UBR,ULB},{DRB,DFR},{DFR,DLF}, {DLF,DBL},{DBL,DRB},{URF,DFR},{DLF,UFL},{DBL,ULB},{UBR,DRB}};
+const Facelet FaceletMove[6] = {
+    {U3,U6,U9,U2,U5,U8,U1,U4,U7,F1,F2,F3,R4,R5,R6,R7,R8,R9,L1,L2,L3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,D7,D8,D9,B1,B2,B3,L4,L5,L6,L7,L8,L9,R1,R2,R3,B4,B5,B6,B7,B8,B9},
+    {U1,U2,B7,U4,U5,B4,U7,U8,B1,R3,R6,R9,R2,R5,R8,R1,R4,R7,F1,F2,U3,F4,F5,U6,F7,F8,U9,D1,D2,F3,D4,D5,F6,D7,D8,F9,L1,L2,L3,L4,L5,L6,L7,L8,L9,D9,B2,B3,D6,B5,B6,D3,B8,B9},
+    {U1,U2,U3,U4,U5,U6,R1,R4,R7,D3,R2,R3,D2,R5,R6,D1,R8,R9,F3,F6,F9,F2,F5,F8,F1,F4,F7,L3,L6,L9,D4,D5,D6,D7,D8,D9,L1,L2,U9,L4,L5,U8,L7,L8,U7,B1,B2,B3,B4,B5,B6,B7,B8,B9},
+    {U1,U2,U3,U4,U5,U6,U7,U8,U9,R1,R2,R3,R4,R5,R6,B7,B8,B9,F1,F2,F3,F4,F5,F6,R7,R8,R9,D3,D6,D9,D2,D5,D8,D1,D4,D7,L1,L2,L3,L4,L5,L6,F7,F8,F9,B1,B2,B3,B4,B5,B6,L7,L8,L9},
+    {F1,U2,U3,F4,U5,U6,F7,U8,U9,R1,R2,R3,R4,R5,R6,R7,R8,R9,D1,F2,F3,D4,F5,F6,D7,F8,F9,B9,D2,D3,B6,D5,D6,B3,D8,D9,L3,L6,L9,L2,L5,L8,L1,L4,L7,B1,B2,U7,B4,B5,U4,B7,B8,U1},
+    {L7,L4,L1,U4,U5,U6,U7,U8,U9,R1,R2,U1,R4,R5,U2,R7,R8,U3,F1,F2,F3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,R9,R6,R3,D7,L2,L3,D8,L5,L6,D9,L8,L9,B3,B6,B9,B2,B5,B8,B1,B4,B7}
+};
+const Facelet FaceletSym[4] = {
+    {R9,R8,R7,R6,R5,R4,R3,R2,R1,F3,F6,F9,F2,F5,F8,F1,F4,F7,U3,U6,U9,U2,U5,U8,U1,U4,U7,L1,L2,L3,L4,L5,L6,L7,L8,L9,B7,B4,B1,B8,B5,B2,B9,B6,B3,D3,D6,D9,D2,D5,D8,D1,D4,D7},
+    {D9,D8,D7,D6,D5,D4,D3,D2,D1,L9,L8,L7,L6,L5,L4,L3,L2,L1,F9,F8,F7,F6,F5,F4,F3,F2,F1,U9,U8,U7,U6,U5,U4,U3,U2,U1,R9,R8,R7,R6,R5,R4,R3,R2,R1,B9,B8,B7,B6,B5,B4,B3,B2,B1},
+    {U3,U6,U9,U2,U5,U8,U1,U4,U7,F1,F2,F3,F4,F5,F6,F7,F8,F9,L1,L2,L3,L4,L5,L6,L7,L8,L9,D7,D4,D1,D8,D5,D2,D9,D6,D3,B1,B2,B3,B4,B5,B6,B7,B8,B9,R1,R2,R3,R4,R5,R6,R7,R8,R9},
+    {U3,U2,U1,U6,U5,U4,U9,U8,U7,L3,L2,L1,L6,L5,L4,L9,L8,L7,F3,F2,F1,F6,F5,F4,F9,F8,F7,D3,D2,D1,D6,D5,D4,D9,D8,D7,R3,R2,R1,R6,R5,R4,R9,R8,R7,B3,B2,B1,B6,B5,B4,B9,B8,B7}
+};
+const CornerCubie CornerCubieMove[6] = {
+    {{UBR,0},{URF,0},{UFL,0},{ULB,0},{DFR,0},{DLF,0},{DBL,0},{DRB,0}},
+    {{DFR,2},{UFL,0},{ULB,0},{URF,1},{DRB,1},{DLF,0},{DBL,0},{UBR,2}},
+    {{UFL,1},{DLF,2},{ULB,0},{UBR,0},{URF,2},{DFR,1},{DBL,0},{DRB,0}},
+    {{URF,0},{UFL,0},{ULB,0},{UBR,0},{DLF,0},{DBL,0},{DRB,0},{DFR,0}},
+    {{URF,0},{ULB,1},{DBL,2},{UBR,0},{DFR,0},{UFL,2},{DLF,1},{DRB,0}},
+    {{URF,0},{UFL,0},{UBR,1},{DRB,2},{DFR,0},{DLF,0},{ULB,2},{DBL,1}}
+};
+const EdgeCubie EdgeCubieMove[6] = {
+    {{UB,0},{UR,0},{UF,0},{UL,0},{DR,0},{DF,0},{DL,0},{DB,0},{FR,0},{FL,0},{BL,0},{BR,0}},
+    {{FR,0},{UF,0},{UL,0},{UB,0},{BR,0},{DF,0},{DL,0},{DB,0},{DR,0},{FL,0},{BL,0},{UR,0}},
+    {{UR,0},{FL,1},{UL,0},{UB,0},{DR,0},{FR,1},{DL,0},{DB,0},{UF,1},{DF,1},{BL,0},{BR,0}},
+    {{UR,0},{UF,0},{UL,0},{UB,0},{DF,0},{DL,0},{DB,0},{DR,0},{FR,0},{FL,0},{BL,0},{BR,0}},
+    {{UR,0},{UF,0},{BL,0},{UB,0},{DR,0},{DF,0},{FL,0},{DB,0},{FR,0},{UL,0},{DL,0},{BR,0}},
+    {{UR,0},{UF,0},{UL,0},{BR,1},{DR,0},{DF,0},{DL,0},{BL,1},{FR,0},{FL,0},{UB,1},{DB,1}}
+};
+const CornerCubie CornerCubieSym[4] = {
+    {{URF,1},{DFR,2},{DLF,1},{UFL,2},{UBR,2},{DRB,1},{DBL,2},{ULB,1}},
+    {{DLF,0},{DFR,0},{DRB,0},{DBL,0},{UFL,0},{URF,0},{UBR,0},{ULB,0}},
+    {{UBR,0},{URF,0},{UFL,0},{ULB,0},{DRB,0},{DFR,0},{DLF,0},{DBL,0}},
+    {{UFL,3},{URF,3},{UBR,3},{ULB,3},{DLF,3},{DFR,3},{DRB,3},{DBL,3}}
+};
+const EdgeCubie EdgeCubieSym[4] = {
+    {{UF,1},{FR,0},{DF,1},{FL,0},{UB,1},{BR,0},{DB,1},{BL,0},{UR,1},{DR,1},{DL,1},{UL,1}},
+    {{DL,0},{DF,0},{DR,0},{DB,0},{UL,0},{UF,0},{UR,0},{UB,0},{FL,0},{FR,0},{BR,0},{BL,0}},
+    {{UB,0},{UR,0},{UF,0},{UL,0},{DB,0},{DR,0},{DF,0},{DL,0},{BR,1},{FR,1},{FL,1},{BL,1}},
+    {{UL,0},{UF,0},{UR,0},{UB,0},{DL,0},{DF,0},{DR,0},{DB,0},{FL,0},{FR,0},{BR,0},{BL,0}}
+};
+enum Constant {
+    GN_HTM      = 20,
+    GN_QTM      = 26,
+    N_MOVE      = 18,
+    N_TWIST     = 2187,
+    N_FLIP      = 2048,
+    N_SLICE     = 495,
+    N_CORNER    = 40320,
+    N_EDGE8     = 40320,
+    N_EDGE4     = 24,
+    N_SYM       = 48,
+    N_SYM_D4h   = 16,
+    EQ_FLIPSLICE= 64430,
+    EQ_CORNER   = 2768,
+};
+using std::size_t;
+using namespace cube::math;
+template<size_t N,typename T=int>
+struct Perm
+{
+    using value_type = T;
+    static constexpr size_t size() { return N; }
+    constexpr Perm() noexcept
+    {
+        for(size_t i = 0; i < N; ++i) X[i] = static_cast<T>(i);
+    }
+    constexpr Perm(const std::array<T,N>& xs) noexcept
+        :X{xs}
+    {
+        assert(isValidPermutation<N>(xs) && "array must be a valid permutation");
+    }
+    constexpr Perm(std::initializer_list<T> xs) noexcept
+    {
+        assert(xs.size() == N && "size mismatch");
+        auto dit = X.begin();
+        for(auto it = xs.begin(); it != xs.end(); it++, dit++) { *dit = *it; }
+        assert(isValidPermutation<N>(X) && "invalid permutation list");
+    }
+    template<typename ArrayLike>
+    constexpr ArrayLike act_right(const ArrayLike& src) const noexcept
+    {
+        return backpermute(src,this->X);
+    }
+    friend constexpr bool operator==(const Perm &lhs, const Perm& rhs) noexcept { return lhs.X == rhs.X; }
+    friend constexpr Perm operator*(const Perm &P, const Perm& Q) noexcept      { return Q.act_right(P); }
+    constexpr Perm&     operator*=(const Perm& rhs) noexcept    { return *this = *this * rhs; }
+    constexpr Perm      operator~() const noexcept              { return { forepermute(Perm::id.X, this->X) }; }
+    constexpr T&        operator[](size_t idx) noexcept         { return X[idx]; }
+    constexpr const T&  operator[](size_t idx) const noexcept   { return X[idx]; }
+    bool parity() const
+    {
+        const auto [fixed, cs] = decomposite(this->X);
+        int s = 1;
+        for(auto &c:cs) s *= (c.size() % 2 == 0) ? -1 : 1;
+        return s == 1;
+    }
+    size_t order() const { return orderOf(X); }
+    constexpr size_t rank() const { return rankOf(X); }
+    static constexpr Perm<N,T> fromRank(size_t i) { return { cube::math::fromRank<T,N>(i) }; }
+    static const Perm id;
+    std::array<T,N> X{};
+};
+template<size_t N,typename T>
+inline constexpr Perm<N,T> Perm<N,T>::id{};
+template<size_t N, size_t L, typename T=int>
+struct CArray
+{
+    using value_type = T;
+    static constexpr size_t period = N;
+    static constexpr size_t size() noexcept { return L; }
+    constexpr CArray() noexcept {}
+    constexpr CArray(const std::array<T,L> &arr) noexcept
+        :xs{arr}
+    {
+        auto n = static_cast<T>(N);
+        for(size_t i = 0; i < L; i++) { xs[i] = (xs[i] % n + n) % n; }
+    }
+    constexpr CArray(std::initializer_list<T> list) noexcept
+    {
+        auto n = static_cast<T>(N);
+        std::size_t i = 0;
+        for(T val : list) { if(i < L) { xs[i++] = (val % n + n) % n; } }
+    }
+    constexpr T& operator[](size_t idx) noexcept             { return xs[idx]; }
+    constexpr const T& operator[](size_t idx) const noexcept { return xs[idx]; }
+    constexpr CArray& operator+=(const CArray& rhs)
+    {
+        for(size_t i = 0; i < L; i++) xs[i] = (xs[i] + rhs.xs[i]) % N;
+        return *this;
+    }
+    friend constexpr bool operator==(const CArray& lhs, const CArray& rhs) noexcept { return lhs.xs == rhs.xs; }
+    friend constexpr CArray operator+(CArray lhs, const CArray& rhs) noexcept { return lhs += rhs; }
+    constexpr CArray operator~() const noexcept
+    {
+        CArray<N,L,T> inv {};
+        auto n = static_cast<T>(N);
+        for(size_t i = 0; i < L; i++) inv[i] = (n - xs[i]) % n;
+        return inv;
+    }
+    constexpr T sum() const noexcept
+    {
+        T s = 0;
+        for(size_t i = 0; i < L; i++) s += xs[i];
+        return s % N;
+    }
+    static const CArray id;
+    std::array<T,L> xs {};
+};
+template<size_t N, size_t L, typename T>
+inline constexpr CArray<N,L,T> CArray<N,L,T>::id{};
+template<size_t N, size_t L, typename T>
+constexpr CArray<N,L,T> operator*(const CArray<N,L,T> &xs, const Perm<L,T> &P)
+{
+    return P.act_right(xs);
+}
+template<size_t N, size_t L, typename T>
+constexpr CArray<N,L,T>& operator*=(CArray<N,L,T> &xs, const Perm<L,T> &P)
+{
+    return xs = P.act_right(xs);
+}
+template<typename T, size_t N, size_t... Ns>
+struct ArrayHelper
+{
+    using type = std::array<typename ArrayHelper<T,Ns...>::type,N>;
+};
+template<typename T, size_t N>
+struct ArrayHelper<T,N>
+{
+    using type = std::array<T,N>;
+};
+template<typename T, size_t... Ns>
+struct NArray
+{
+    using value_type = T;
+    static constexpr size_t size = (Ns * ...);
+    static constexpr size_t dim = sizeof...(Ns);
+    static constexpr std::array<size_t,dim> shape { Ns... };
+    typename ArrayHelper<T,Ns...>::type storage;
+    auto& operator[](size_t i) noexcept             { return storage[i]; }
+    const auto& operator[](size_t i) const noexcept { return storage[i]; }
+    template<typename U, std::enable_if_t<std::is_integral_v<U>,int> = 0>
+    auto& operator[](const std::array<U,dim>& indices) noexcept
+    {
+        return std::apply(
+            [this](auto... args) -> decltype(auto) { return (*this)(args...); },
+            indices
+        );
+    }
+    template<typename U, std::enable_if_t<std::is_integral_v<U>,int> = 0>
+    const auto& operator[](const std::array<U,dim>& indices) const noexcept
+    { return const_cast<NArray*>(this)->operator[](indices); }
+    template<typename... Is, std::enable_if_t<sizeof...(Is)==dim, int> = 0>
+    decltype(auto) operator()(Is... is) noexcept
+    { return at<0>(storage, {size_t(is)...}); }
+    template<typename... Is, std::enable_if_t<sizeof...(Is)==dim, int> = 0>
+    decltype(auto) operator()(Is... is) const noexcept
+    { return at<0>(storage, {size_t(is)...}); }
+    template<typename F> void for_each(F&& f)       { for_each_rec(storage, f); }
+    template<typename F> void for_each(F&& f) const { for_each_rec(storage, f); }
+    void fill(const T &val) { for_each([&val](T& e){ e = val; }); }
+private:
+    template<size_t I, typename Arr>
+    static decltype(auto) at(Arr&& arr, const std::array<size_t,dim>& indices)
+    {
+        if constexpr (I+1==dim) return arr[indices[I]];
+        else                    return at<I+1>(arr[indices[I]], indices);
+    }
+    template<typename Arr, typename F>
+    static void for_each_rec(Arr&& arr, F&& f)
+    {
+        for(auto& e: arr)
+        {
+            if constexpr (std::is_same_v<std::remove_reference_t<decltype(e)>,T>) {
+                f(e);
+            } else {
+                for_each_rec(e,f);
+            }
+        }
+    }
+};
+}
+namespace cube {
+using namespace cube::data;
+typedef std::int8_t cube_value_t;
+typedef Perm<54,    cube_value_t>   FacePerm;
+typedef Perm<8,     cube_value_t>   CornerPerm;
+typedef Perm<12,    cube_value_t>   EdgePerm;
+typedef CArray<3, 8,cube_value_t>   CornerOri;
+typedef CArray<2,12,cube_value_t>   EdgeOri;
+struct ColorState;
+struct FaceCube;
+struct CubieCube;
+constexpr CornerPerm operator*(const CornerPerm &cp, const CubieCube &cc);
+constexpr CornerOri  operator*(const CornerOri  &co, const CubieCube &cc);
+constexpr EdgePerm   operator*(const EdgePerm   &ep, const CubieCube &cc);
+constexpr EdgeOri    operator*(const EdgeOri    &eo, const CubieCube &cc);
+struct ColorState
+{
+    std::array<Layer,54> s;
+    Layer & operator[](size_t i) noexcept           { return s[i]; }
+    const Layer operator[](size_t i) const noexcept { return s[i]; }
+    static ColorState fromString(std::string_view cube);
+    static bool is_valid_config(std::string_view cube);
+    FaceCube  toFaceCube() const;
+    CubieCube toCubieCube() const;
+    static const ColorState id;
+};
+inline const ColorState ColorState::id = {{U,U,U,U,U,U,U,U,U,R,R,R,R,R,R,R,R,R,F,F,F,F,F,F,F,F,F,D,D,D,D,D,D,D,D,D,L,L,L,L,L,L,L,L,L,B,B,B,B,B,B,B,B,B}};
+struct FaceCube
+{
+    FacePerm f;
+    CubieCube toCubieCube() const;
+    friend constexpr FaceCube operator*(const FaceCube &lhs, const FaceCube &rhs)
+    { return { lhs.f * rhs.f }; }
+    constexpr FaceCube& operator*=(const FaceCube &rhs)
+    { return *this = *this * rhs; }
+    constexpr FaceCube operator~() const
+    { return { ~f }; }
+    friend bool operator==(const FaceCube &lhs, const FaceCube &rhs)
+    { return lhs.f == rhs.f; }
+    static const FaceCube id;
+};
+struct CubieCube
+{
+    CornerPerm  cp;
+    CornerOri   co;
+    EdgePerm    ep;
+    EdgeOri     eo;
+    FaceCube toFaceCube() const;
+    bool isSolvable() const
+    {  return cp.parity() == ep.parity() && co.sum() == 0 && eo.sum() == 0; }
+    friend constexpr CubieCube operator*(const CubieCube &a, const CubieCube &b)
+    { return { a.cp*b, a.co*b, a.ep*b, a.eo*b }; }
+    constexpr CubieCube& operator*=(const CubieCube &rhs)
+    { return *this = *this * rhs; }
+    constexpr CubieCube operator~() const
+    {
+        auto cp_inv = ~cp;
+        auto ep_inv = ~ep;
+        return { cp_inv, ~(co*cp_inv), ep_inv, ~(eo*ep_inv) };
+    }
+    friend bool operator==(const CubieCube &lhs, const CubieCube &rhs)
+    { return lhs.cp == rhs.cp && lhs.co == rhs.co && lhs.ep == rhs.ep && lhs.eo == rhs.eo; }
+    static const CubieCube id;
+};
+inline constexpr CornerPerm operator*(const CornerPerm &cp, const CubieCube &cc) { return cp * cc.cp; }
+inline constexpr CornerOri  operator*(const CornerOri &co,  const CubieCube &cc) { return co * cc.cp + cc.co; }
+inline constexpr EdgePerm   operator*(const EdgePerm &ep,   const CubieCube &cc) { return ep * cc.ep; }
+inline constexpr EdgeOri    operator*(const EdgeOri &eo,    const CubieCube &cc) { return eo * cc.ep + cc.eo; }
+inline constexpr CornerPerm    eCP = {0,1,2,3,4,5,6,7};
+inline constexpr EdgePerm      eEP = {0,1,2,3,4,5,6,7,8,9,10,11};
+inline constexpr CornerOri     eCO = {0,0,0,0,0,0,0,0};
+inline constexpr EdgeOri       eEO = {0,0,0,0,0,0,0,0,0,0,0,0};
+inline constexpr FacePerm      eFP = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53};
+inline constexpr FaceCube   FaceCube::id = FaceCube{eFP};
+inline constexpr CubieCube  CubieCube::id = CubieCube{eCP,eCO,eEP,eEO};
+inline constexpr CubieCube mU = {{3,0,1,2,4,5,6,7},{0,0,0,0,0,0,0,0},{3,0,1,2,4,5,6,7,8,9,10,11},{0,0,0,0,0,0,0,0,0,0,0,0}};
+inline constexpr CubieCube mR = {{4,1,2,0,7,5,6,3},{2,0,0,1,1,0,0,2},{8,1,2,3,11,5,6,7,4,9,10,0},{0,0,0,0,0,0,0,0,0,0,0,0}};
+inline constexpr CubieCube mF = {{1,5,2,3,0,4,6,7},{1,2,0,0,2,1,0,0},{0,9,2,3,4,8,6,7,1,5,10,11},{0,1,0,0,0,1,0,0,1,1,0,0}};
+inline constexpr CubieCube mD = {{0,1,2,3,5,6,7,4},{0,0,0,0,0,0,0,0},{0,1,2,3,5,6,7,4,8,9,10,11},{0,0,0,0,0,0,0,0,0,0,0,0}};
+inline constexpr CubieCube mL = {{0,2,6,3,4,1,5,7},{0,1,2,0,0,2,1,0},{0,1,10,3,4,5,9,7,8,2,6,11},{0,0,0,0,0,0,0,0,0,0,0,0}};
+inline constexpr CubieCube mB = {{0,1,3,7,4,5,2,6},{0,0,1,2,0,0,2,1},{0,1,2,11,4,5,6,10,8,9,3,7},{0,0,0,1,0,0,0,1,0,0,1,1}};
+inline constexpr std::array<CubieCube,18>
+    ElementaryMove = { mU,mU*mU,mU*mU*mU,mR,mR*mR,mR*mR*mR,mF,mF*mF,mF*mF*mF,mD,mD*mD,mD*mD*mD,mL,mL*mL,mL*mL*mL,mB,mB*mB,mB*mB*mB };
+inline constexpr FaceCube pU = {{6,3,0,7,4,1,8,5,2,45,46,47,12,13,14,15,16,17,9,10,11,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,18,19,20,39,40,41,42,43,44,36,37,38,48,49,50,51,52,53}};
+inline constexpr FaceCube pR = {{0,1,20,3,4,23,6,7,26,15,12,9,16,13,10,17,14,11,18,19,29,21,22,32,24,25,35,27,28,51,30,31,48,33,34,45,36,37,38,39,40,41,42,43,44,8,46,47,5,49,50,2,52,53}};
+inline constexpr FaceCube pF = {{0,1,2,3,4,5,44,41,38,6,10,11,7,13,14,8,16,17,24,21,18,25,22,19,26,23,20,15,12,9,30,31,32,33,34,35,36,37,27,39,40,28,42,43,29,45,46,47,48,49,50,51,52,53}};
+inline constexpr FaceCube pD = {{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,24,25,26,18,19,20,21,22,23,42,43,44,33,30,27,34,31,28,35,32,29,36,37,38,39,40,41,51,52,53,45,46,47,48,49,50,15,16,17}};
+inline constexpr FaceCube pL = {{53,1,2,50,4,5,47,7,8,9,10,11,12,13,14,15,16,17,0,19,20,3,22,23,6,25,26,18,28,29,21,31,32,24,34,35,42,39,36,43,40,37,44,41,38,45,46,33,48,49,30,51,52,27}};
+inline constexpr FaceCube pB = {{11,14,17,3,4,5,6,7,8,9,10,35,12,13,34,15,16,33,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,36,39,42,2,37,38,1,40,41,0,43,44,51,48,45,52,49,46,53,50,47}};
+inline constexpr std::array<FaceCube,18>
+    ElementaryPerm = { pU,pU*pU,pU*pU*pU,pR,pR*pR,pR*pR*pR,pF,pF*pF,pF*pF*pF,pD,pD*pD,pD*pD*pD,pL,pL*pL,pL*pL*pL,pB,pB*pB,pB*pB*pB };
+inline constexpr std::array<TurnMove,N_MOVE> turn_entries = { Ux1,Ux2,Ux3,Rx1,Rx2,Rx3,Fx1,Fx2,Fx3,Dx1,Dx2,Dx3,Lx1,Lx2,Lx3,Bx1,Bx2,Bx3 };
+inline constexpr std::array<TurnMove,N_MOVE> turn_inv_entries = { Ux3,Ux2,Ux1,Rx3,Rx2,Rx1,Fx3,Fx2,Fx1,Dx3,Dx2,Dx1,Lx3,Lx2,Lx1,Bx3,Bx2,Bx1 };
+inline constexpr TurnMove operator~(TurnMove m) { return turn_inv_entries[m]; }
+inline FaceCube operator*(const FaceCube &c, const std::vector<TurnMove> &ms)
+{
+    FaceCube fc = c;
+    for (auto m : ms) fc = fc * ElementaryPerm[m];
+    return fc;
+}
+inline CubieCube operator*(const CubieCube &c, const std::vector<TurnMove> &ms)
+{
+    CubieCube cc = c;
+    for (auto m : ms) cc = cc * ElementaryMove[m];
+    return cc;
+}
+inline ColorState operator*(const ColorState &c, const std::vector<TurnMove> &ms)
+{
+    ColorState cs = c;
+    for (auto m : ms) cs = ElementaryPerm[m].f.act_right(cs);
+    return cs;
+}
+}
+namespace cube {
+using namespace cube::data;
+ColorState ColorState::fromString(std::string_view cube)
+{
+    assert(is_valid_config(cube) && "invalid cube config");
+    std::array<Layer,54> xs{};
+    std::transform(cube.cbegin(), cube.cend(), xs.begin(), [](char c) {
+        switch(c) {
+        case 'U': return U;
+        case 'R': return R;
+        case 'F': return F;
+        case 'D': return D;
+        case 'L': return L;
+        case 'B': return B;
+        default: throw std::invalid_argument("invalid cube config");
+        }
+    });
+    return { xs };
+}
+bool ColorState::is_valid_config(std::string_view cfg)
+{
+    if(cfg.size() != 54) return false;
+    std::set<char> vs { cfg[CC[0]],cfg[CC[1]],cfg[CC[2]],cfg[CC[3]],cfg[CC[4]],cfg[CC[5]] };
+    if(vs.size() != 6) return false;
+    for(size_t i = 0, x = 0; i < 8; i++) {
+        for(x = 0; x < 24; x++) {
+            if(cfg[CC[CCI[i][0]]] == cfg[CF[x/3][x%3]]
+               && cfg[CC[CCI[i][1]]] == cfg[CF[x/3][(x+1)%3]]
+               && cfg[CC[CCI[i][2]]] == cfg[CF[x/3][(x+2)%3]]) break;
+        }
+        if(x >= 24) return false;
+    }
+    for(size_t i = 0, y = 0; i < 12; i++) {
+        for(y = 0; y < 24; y++) {
+            if(cfg[CC[ECI[i][0]]] == cfg[EF[y/2][y%2]]
+               && cfg[CC[ECI[i][1]]] == cfg[EF[y/2][(y+1)%2]]) break;
+        }
+        if(y >= 24) return false;
+    }
+    return true;
+}
+FaceCube ColorState::toFaceCube() const
+{
+    FacePerm fp{};
+    for(int i = 0; i < 6; ++i) {
+        fp[CC[i]] = s[CC[i]];
+    }
+    for(int i = 0; i < 8; i++) {
+        for(int x = 0; x < 24; x++) {
+            if(s[CC[CCI[i][0]]] == s[CF[x/3][x%3]]
+            && s[CC[CCI[i][1]]] == s[CF[x/3][(x+1)%3]]
+            && s[CC[CCI[i][2]]] == s[CF[x/3][(x+2)%3]])
+            {
+                fp[CF[x/3][x%3]]      = CF[i][0];
+                fp[CF[x/3][(x+1)%3]]  = CF[i][1];
+                fp[CF[x/3][(x+2)%3]]  = CF[i][2];
+                break;
+            }
+        }
+    }
+    for(int i = 0; i < 12; i++) {
+        for(int y = 0; y < 24; y++) {
+            if(s[CC[ECI[i][0]]] == s[EF[y/2][y%2]]
+            && s[CC[ECI[i][1]]] == s[EF[y/2][(y+1)%2]])
+            {
+                fp[EF[y/2][y%2]]      = EF[i][0];
+                fp[EF[y/2][(y+1)%2]]  = EF[i][1];
+                break;
+            }
+        }
+    }
+    return { fp };
+}
+CubieCube ColorState::toCubieCube() const
+{
+    return toFaceCube().toCubieCube();
+}
+CubieCube FaceCube::toCubieCube() const
+{
+    CubieCube cc{};
+    const FaceCube &fc = *this;
+    for(int i = 0; i < 8; i++) {
+        for(int x = 0; x < 24; x++) {
+            if(fc.f[CF[i][0]] == CF[x/3][x%3]) {
+                cc.cp[i] = x/3, cc.co[i] = (3-x%3)%3;
+                break;
+            }
+        }
+    }
+    for(int i = 0; i < 12; i++){
+        for(int y = 0; y < 24; y++){
+            if(fc.f[EF[i][0]] == EF[y/2][y%2]) {
+                cc.ep[i] = y/2, cc.eo[i] = (2-y%2)%2;
+                break;
+            }
+        }
+    }
+    return cc;
+}
+FaceCube CubieCube::toFaceCube() const
+{
+    FaceCube fc{};
+    const CubieCube &cc = *this;
+    for(int i = 0; i < 6; i++) {
+        fc.f[CC[i]] = CC[i];
+    }
+    for(int i = 0; i < 8; i++) for(int j = 0; j < 3; j++) {
+        fc.f[CF[i][j]] = CF[cc.cp[i]][(j-cc.co[i]+3)%3];
+    }
+    for(int i = 0; i < 12; i++) for(int j = 0; j < 2; j++) {
+        fc.f[EF[i][j]] = EF[cc.ep[i]][(j-cc.eo[i]+2)%2];
+    }
+    return fc;
+}
+}
+namespace cube::pdb {
+struct Coord
+{
+    int twist, flip, slice;
+    int corner, edge4, edge8;
+    static const Coord id;
+    static CornerOri    twist2co(int);
+    static int          co2twist(const CornerOri &);
+    static EdgeOri      flip2eo(int);
+    static int          eo2flip(const EdgeOri &);
+    static CornerPerm   corner2cp(int);
+    static int          cp2corner(const CornerPerm &);
+    static int          ep2slice(const EdgePerm &);
+    static int          ep2edge4(const EdgePerm &);
+    static int          ep2edge8(const EdgePerm &);
+    static EdgePerm     slice2ep(int);
+    static EdgePerm     edge42ep(int);
+    static EdgePerm     edge82ep(int);
+    static EdgePerm     see2ep(int,int,int);
+    static Coord        CubieCube2Coord(const CubieCube &);
+    static CubieCube    Coord2CubieCube(const Coord &);
+};
+constexpr bool operator==(const Coord &c1, const Coord &c2)
+{
+    return c1.twist == c2.twist && c1.flip == c2.flip && c1.slice == c2.slice
+        && c1.corner == c2.corner && c1.edge4 == c2.edge4 && c1.edge8 == c2.edge8;
+}
+inline constexpr Coord Coord::id = { 0,0,0,0,0,0 };
+template<size_t coord_size>
+using MoveTableLayout = NArray<uint16_t,N_MOVE,coord_size>;
+using MoveTwist = MoveTableLayout<N_TWIST>;
+using MoveFlip  = MoveTableLayout<N_FLIP>;
+using MoveSlice = MoveTableLayout<N_SLICE>;
+using MoveEdge4 = MoveTableLayout<N_EDGE4>;
+using MoveEdge8 = MoveTableLayout<N_EDGE8>;
+using MoveCorner= MoveTableLayout<N_CORNER>;
+inline int transform_twist(TurnMove m, int twist)   { return Coord::co2twist(Coord::twist2co(twist) * ElementaryMove[m]); }
+inline int transform_flip(TurnMove m, int flip)     { return Coord::eo2flip(Coord::flip2eo(flip) * ElementaryMove[m]); }
+inline int transform_slice(TurnMove m, int slice)   { return Coord::ep2slice(Coord::slice2ep(slice) * ElementaryMove[m]); }
+inline int transform_corner(TurnMove m, int corner) { return Coord::cp2corner(Coord::corner2cp(corner) * ElementaryMove[m]); }
+inline int transform_edge4(TurnMove m, int edge4)   { return Coord::ep2edge4(Coord::edge42ep(edge4) * ElementaryMove[m]); }
+inline int transform_edge8(TurnMove m, int edge8)   { return Coord::ep2edge8(Coord::edge82ep(edge8) * ElementaryMove[m]); }
+}
+namespace cube::pdb {
+using namespace cube::math;
+using namespace cube::data;
+inline bool isSliceEdge(size_t idx)
+{
+    return FR <= idx && idx <= BR;
+}
+int Coord::co2twist(const CornerOri &co)
+{
+    return int(fromDigits<3>(takeByRange<1,7>(co.xs)));
+}
+CornerOri Coord::twist2co(int i)
+{
+    CornerOri co;
+    auto sub1to7 = CArray<3,7,CornerOri::value_type>{toDigits<3,7,CornerOri::value_type>(i)};
+    std::copy(sub1to7.xs.begin(),sub1to7.xs.end(),co.xs.begin()+1);
+    co[0] = (3 - sub1to7.sum()) % 3;
+    return co;
+}
+int Coord::eo2flip(const EdgeOri &eo)
+{
+    return int(fromDigits<2>(takeByRange<1,11>(eo.xs)));
+}
+EdgeOri Coord::flip2eo(int i)
+{
+    EdgeOri eo;
+    auto sub1to11 = CArray<2,11,CornerOri::value_type>{toDigits<2,11,CornerOri::value_type>(i)};
+    std::copy(sub1to11.xs.begin(),sub1to11.xs.end(),eo.xs.begin()+1);
+    eo[0] = (2 - sub1to11.sum()) % 2;
+    return eo;
+}
+int Coord::ep2slice(const EdgePerm &ep)
+{
+    int x[4],slice=0,N=12;
+    for(int i = 0, j = 0; i < N; i++) {
+        if(isSliceEdge(ep[i])) x[j++] = i;
+    }
+    for(int i = 0; i < 4; i++) slice += binomial(N-1-x[i],4-i);
+    return slice;
+}
+int Coord::ep2edge4(const EdgePerm &ep)
+{
+    Perm<4,EdgePerm::value_type> edge4_perm;
+    for(int i = 0, j = 0; i < 4; i++, j++) {
+        while(!isSliceEdge(ep[j])) j++;
+        edge4_perm[i] = ep[j] - 8;
+    }
+    return int(edge4_perm.rank());
+}
+int Coord::ep2edge8(const EdgePerm &ep)
+{
+    Perm<8,EdgePerm::value_type> edge8_perm;
+    for(size_t i = 0, j = 0; i < 8; i++, j++) {
+        while(isSliceEdge(ep[j]) || ep[j] == (EdgePerm::value_type) ~0UL) j++;
+        edge8_perm[i] = ep[j] - 0;
+    }
+    return int(edge8_perm.rank());
+}
+int Coord::cp2corner(const CornerPerm &cp)
+{
+    return int(cp.rank());
+}
+CornerPerm Coord::corner2cp(int i)
+{
+    return CornerPerm::fromRank(i);
+}
+EdgePerm Coord::slice2ep(int i)
+{
+    auto si = lexicalOrderToIndices<12,4>(i);
+    EdgePerm ep;
+    for(size_t i = 0, j = 0, k = 0; i < 12; i++) {
+        ep[i] = (j < 4 && i == si[j]) ? 8 + j++ : k++;
+    }
+    return ep;
+}
+EdgePerm Coord::edge42ep(int i)
+{
+    EdgePerm ep;
+    auto e4 = Perm<4,EdgePerm::value_type>::fromRank(i);
+    for(size_t i = 0; i < 12; i++) {
+        ep[i] = (i < 8) ? i : e4[i-8] + 8;
+    }
+    return ep;
+}
+EdgePerm Coord::edge82ep(int i)
+{
+    EdgePerm ep;
+    auto e8 = Perm<8,EdgePerm::value_type>::fromRank(i);
+    for(size_t i = 0; i < 12; i++) {
+        ep[i] = (i < 8) ? e8[i] + 0 : i;
+    }
+    return ep;
+}
+EdgePerm Coord::see2ep(int slice, int edge4, int edge8)
+{
+    auto e4 = Perm<4,EdgePerm::value_type>::fromRank(edge4);
+    auto e8 = Perm<8,EdgePerm::value_type>::fromRank(edge8);
+    auto s = lexicalOrderToIndices<12,4>(slice);
+    EdgePerm ep;
+    for(size_t i = 0, j = 0, x = 0, y = 0; i < 12; i++) {
+        ep[i] = (j >=4 || i != s[j]) ? e8[y++]+0 : (++j,e4[x++]+8);
+    }
+    return ep;
+}
+Coord Coord::CubieCube2Coord(const CubieCube &cc)
+{
+    return {
+        Coord::co2twist(cc.co),
+        Coord::eo2flip(cc.eo),
+        Coord::ep2slice(cc.ep),
+        Coord::cp2corner(cc.cp),
+        Coord::ep2edge4(cc.ep),
+        Coord::ep2edge8(cc.ep)
+    };
+}
+CubieCube Coord::Coord2CubieCube(const Coord &c)
+{
+    return {
+        Coord::corner2cp(c.corner),
+        Coord::twist2co(c.twist),
+        Coord::see2ep(c.slice,c.edge4,c.edge8),
+        Coord::flip2eo(c.flip)
+    };
+}
+}
+namespace cube::config {
+void set_table_dir(std::filesystem::path dir);
+auto get_table_dir() -> std::filesystem::path;
+bool use_symmetry();
+void use_symmetry(bool v);
+}
+namespace cube::symmetry {
+using cube::CubieCube;
+using namespace cube::pdb;
+inline constexpr CubieCube S_U4 = {{3,0,1,2,7,4,5,6},{0,0,0,0,0,0,0,0},{3,0,1,2,7,4,5,6,11,8,9,10},{0,0,0,0,0,0,0,0,1,1,1,1}};
+inline constexpr CubieCube S_RL = {{1,0,3,2,5,4,7,6},{0,0,0,0,0,0,0,0},{2,1,0,3,6,5,4,7,9,8,11,10},{0,0,0,0,0,0,0,0,0,0,0,0}};
+inline constexpr CubieCube S_F2 = {{5,4,7,6,1,0,3,2},{0,0,0,0,0,0,0,0},{6,5,4,7,2,1,0,3,9,8,11,10},{0,0,0,0,0,0,0,0,0,0,0,0}};
+enum Sym: uint8_t
+{
+    Id, U1, U2, U3,
+    RL, RL_U1, RL_U2, RL_U3,
+    FF, FF_U1, FF_U2, FF_U3,
+    FF_RL, FF_RL_U1, FF_RL_U2, FF_RL_U3
+};
+constexpr bool is_mirror(Sym s) { return (RL <= s && s <= RL_U3) || (FF_RL <= s && s <= FF_RL_U3); }
+inline constexpr std::array<Sym,N_SYM_D4h> sym_entries      = { Id,U1,U2,U3,RL,RL_U1,RL_U2,RL_U3,FF,FF_U1,FF_U2,FF_U3,FF_RL,FF_RL_U1,FF_RL_U2,FF_RL_U3 };
+inline constexpr std::array<Sym,N_SYM_D4h> sym_inv_entries  = { Id,U3,U2,U1,RL,RL_U1,RL_U2,RL_U3,FF,FF_U1,FF_U2,FF_U3,FF_RL,FF_RL_U3,FF_RL_U2,FF_RL_U1 };
+constexpr Sym operator~(Sym s) { return sym_inv_entries[s]; }
+inline constexpr std::array<CubieCube,N_SYM_D4h> SymmetricMove = {
+    CubieCube::id, S_U4, S_U4*S_U4, S_U4*S_U4*S_U4,
+    S_RL, S_RL*S_U4, S_RL*S_U4*S_U4, S_RL*S_U4*S_U4*S_U4,
+    S_F2, S_F2*S_U4, S_F2*S_U4*S_U4, S_F2*S_U4*S_U4*S_U4,
+    S_F2*S_RL, S_F2*S_RL*S_U4, S_F2*S_RL*S_U4*S_U4, S_F2*S_RL*S_U4*S_U4*S_U4
+};
+typedef uint16_t                    corner_t;
+typedef std::array<uint16_t,2>      flipslice_t;
+typedef struct{uint16_t eq; Sym s;} corner_eq_t;
+typedef struct{uint16_t eq; Sym s;} flipslice_eq_t;
+template<size_t coord_size>
+using ConjTableLayout = NArray<uint16_t,N_SYM_D4h,coord_size>;
+using TwistConj     = ConjTableLayout<N_TWIST>;
+using SliceConj     = NArray<uint16_t,N_SYM_D4h,N_SLICE>;
+using Edge4Conj     = NArray<uint16_t,N_SYM_D4h,N_EDGE4>;
+using Edge8Conj     = NArray<uint16_t,N_SYM_D4h,N_EDGE8>;
+using CornerConj    = NArray<uint16_t,N_SYM_D4h,N_CORNER>;
+using FlipSliceConj = NArray<flipslice_t,N_SYM_D4h,N_FLIP,N_SLICE>;
+struct SymReduction
+{
+    using CornerDom     = NArray<corner_eq_t,N_CORNER>;
+    using EqCornerDom   = NArray<corner_t,EQ_CORNER>;
+    using FlipSliceDom  = NArray<flipslice_eq_t,N_FLIP,N_SLICE>;
+    using EqFlipSliceDom= NArray<flipslice_t,EQ_FLIPSLICE>;
+    typedef uint16_t SymMask;
+    using CornerStab    = NArray<SymMask,EQ_CORNER>;
+    using FlipSliceStab = NArray<SymMask,EQ_FLIPSLICE>;
+public:
+    static auto make_trans_edge8_corner(
+        const CornerDom& emb, const EqCornerDom& rep, const Edge8Conj& conj,
+        const MoveEdge8& tran_edge8, const MoveCorner& tran_corner
+    ) {
+        return [&](TurnMove m, const std::pair<uint16_t,uint16_t>& ec) -> std::pair<uint16_t,uint16_t> {
+            auto [edge8, c_corner] = ec;
+            auto corner = rep(c_corner);
+            auto [eq, s] = emb(tran_corner(m,corner));
+            return { conj(~s,tran_edge8(m,edge8)), eq };
+        };
+    }
+    static auto make_trans_twist_flipslice(
+        const FlipSliceDom& emb, const EqFlipSliceDom& rep, const TwistConj& conj,
+        const MoveTwist& tran_twist, const MoveFlip& tran_flip, const MoveSlice& tran_slice
+    ) {
+        return [&](TurnMove m, const std::pair<uint16_t,uint16_t>& tfs) -> std::pair<uint16_t,uint16_t> {
+            auto [twist, c_flipslice] = tfs;
+            auto [flip, slice] = rep(c_flipslice);
+            auto [eq, s] = emb(tran_flip(m,flip),tran_slice(m,slice));
+            return { conj(~s,tran_twist(m,twist)), eq };
+        };
+    }
+    static void build_emb_corner(CornerDom&, const CornerConj&);
+    static void build_rep_corner(EqCornerDom&, const CornerDom&);
+    static void build_emb_flipslice(FlipSliceDom&, const FlipSliceConj&);
+    static void build_rep_flipslice(EqFlipSliceDom&, const FlipSliceDom&);
+    static void build_stab_corner(CornerStab&, const EqCornerDom&, const CornerConj&);
+    static void build_stab_flipslice(FlipSliceStab&, const EqFlipSliceDom&, const FlipSliceConj&);
+};
+inline int conjugate_slice(Sym s, int slice)    { return Coord::ep2slice(SymmetricMove[~s].ep * Coord::slice2ep(slice) * SymmetricMove[s].ep); }
+inline int conjugate_edge4(Sym s, int edge4)    { return Coord::ep2edge4(SymmetricMove[~s].ep * Coord::edge42ep(edge4) * SymmetricMove[s].ep); }
+inline int conjugate_edge8(Sym s, int edge8)    { return Coord::ep2edge8(SymmetricMove[~s].ep * Coord::edge82ep(edge8) * SymmetricMove[s].ep); }
+inline int conjugate_corner(Sym s, int corner)  { return Coord::cp2corner(SymmetricMove[~s].cp * Coord::corner2cp(corner) * SymmetricMove[s].cp); }
+inline int conjugate_twist(Sym s, int twist)
+{
+    auto co = Coord::twist2co(twist) * SymmetricMove[s];
+    return Coord::co2twist(is_mirror(s) ? ~co : co);
+}
+inline flipslice_t conjugate_flipslice(Sym s, const flipslice_t& fs)
+{
+    auto c = CubieCube { {},{},Coord::slice2ep(fs[1]),Coord::flip2eo(fs[0]) };
+    c = SymmetricMove[~s] * c * SymmetricMove[s];
+    return { uint16_t(Coord::eo2flip(c.eo)), uint16_t(Coord::ep2slice(c.ep)) };
+}
+}
+namespace cube::pdb::table {
+using namespace cube::data;
+using namespace cube::symmetry;
+#if CUBE_ENABLE_TRACE
+#   define VTRACE(...) fprintf(stderr,__VA_ARGS__)
+#else
+#   define VTRACE(...) do{}while(0)
+#endif
+template<size_t coord_size>
+struct MoveTableBase: MoveTableLayout<coord_size>
+{
+    static constexpr size_t size = coord_size;
+    template<typename F> void build(F&& tran_fn)
+    {
+        for(size_t x = 0; x < coord_size; ++x)
+            for(auto m: turn_entries)
+                (*this)(m,x) = tran_fn(m,x);
+    }
+};
+template<size_t coord_size>
+struct ConjTableBase: ConjTableLayout<coord_size>
+{
+    static constexpr size_t size = coord_size;
+    template<typename F> void build(F&& conj_fn)
+    {
+        for(size_t x = 0; x < coord_size; ++x)
+            for(auto s: sym_entries)
+                (*this)[s][x] = conj_fn(s,x);
+    }
+};
+template<size_t... Ns>
+struct PrunTableBase: NArray<uint8_t,Ns...>
+{
+    using index_type = std::array<size_t,PrunTableBase::dim>;
+    template<typename...MTs>
+    void build_disjU(MTs&&... tables)
+    {
+        static_assert(sizeof...(tables) == PrunTableBase::dim);
+        auto ts = std::tie(tables...);
+        auto next = [ts](TurnMove m, const index_type&c) -> index_type {
+            index_type c_next;
+            size_t idx = 0;
+            std::apply([&](auto&&... tbl){
+                ( (c_next[idx] = tbl[m][c[idx]], ++idx),... );
+            },ts);
+            return c_next;
+        };
+        auto orbit = [](const index_type& c, auto&& visit) { visit(c); };
+        build(next,orbit);
+    }
+    template<typename TranX, typename ConjX,
+             typename TranY, typename EmbY, typename RepY, typename StabY>
+    void build_XxEqY(TranX&& Tx, ConjX&& conj, TranY&& Ty, EmbY&& emb, RepY&& rep, StabY&& stab)
+    {
+        static_assert(sizeof...(Ns) == 2 && "only dim=2 is implemented");
+        auto next = [&](TurnMove m, const index_type& c) -> index_type {
+            auto [x, eqY] = c;
+            auto y = rep(eqY);
+            auto [eq, s] = emb[Ty(m,y)];
+            return { conj(~s,Tx(m,x)), eq };
+        };
+        auto orbit = [&](const index_type& c, auto&& visit) {
+            const auto [x,eqY] = c;
+            for(auto s: sym_entries)
+            {
+                if(stab(eqY) & (1u << s)) visit(index_type{conj(s,x),eqY});
+            }
+        };
+        build(next,orbit);
+    }
+    template<typename NextFn, typename OrbitFn>
+    void build(NextFn&& next_fn, OrbitFn&& orbit_fn)
+    {
+        constexpr auto unvisited = std::numeric_limits<uint8_t>::max();
+        this->fill(unvisited);
+        std::queue<index_type> q;
+        size_t discovered = 0, depth = 0;
+        auto visit_fn = [&](const index_type& c) {
+            auto& entry = (*this)[c];
+            if(entry == unvisited) { entry = depth; ++discovered; }
+        };
+        index_type starting {};
+        orbit_fn(starting,visit_fn);
+        q.push(starting);
+        while(!q.empty())
+        {
+            VTRACE("[depth %3zu] visited: %9zu / %-9zu\n",
+                depth, discovered, PrunTableBase::size
+            );
+            ++depth;
+            const auto n_depth = q.size();
+            for(auto _ = 0; _ < n_depth; ++_)
+            {
+                auto c = q.front(); q.pop();
+                for(auto m: turn_entries) {
+                    const auto c_next = next_fn(m,c);
+                    if((*this)[c_next] == unvisited) {
+                        orbit_fn(c_next,visit_fn);
+                        q.push(c_next);
+                    }
+                }
+            }
+        }
+    }
+};
+}
+#define TABLE_MAGIC 8677391316305539925
+#define TABLE_VERSION 1
+namespace cube::pdb::table {
+namespace fs = std::filesystem;
+template<typename> inline constexpr bool always_false_v = false;
+enum class MoveKind { Twist, Flip, Slice, Edge4, Edge8, Corner, Count };
+enum class ConjKind { TwistConj, SliceConj, Edge4Conj, Edge8Conj, CornerConj, FlipSliceConj, Count };
+enum class EqKind   { RepCorner, RepFlipSlice, EmbCorner, EmbFlipSlice, Count };
+enum class StabKind { CornerStab, FlipSliceStab, Count };
+enum class PrunKind { SliceFlip, SliceTwist, Edge4Edge8, Edge4Corner, Edge8EqCorner, TwistEqFlipSlice, Count };
+template<auto K> struct table_meta;
+template<auto K> using table_t = typename table_meta<K>::type;
+template<auto K> constexpr std::string_view tag_v = table_meta<K>::value;
+#define Unwrap(...) __VA_ARGS__
+#define spec_table_meta(Kind,Type,Tag) \
+    template<> struct table_meta<Kind> \
+    {                                  \
+        using type = Unwrap Type;      \
+        static constexpr std::string_view value = Tag; \
+    };
+spec_table_meta(MoveKind::Twist,        (MoveTableBase<N_TWIST>),       "TmTwist")
+spec_table_meta(MoveKind::Flip,         (MoveTableBase<N_FLIP>),        "TmFlip")
+spec_table_meta(MoveKind::Slice,        (MoveTableBase<N_SLICE>),       "TmSlice")
+spec_table_meta(MoveKind::Edge4,        (MoveTableBase<N_EDGE4>),       "TmEdge4")
+spec_table_meta(MoveKind::Edge8,        (MoveTableBase<N_EDGE8>),       "TmEdge8")
+spec_table_meta(MoveKind::Corner,       (MoveTableBase<N_CORNER>),      "TmCorner")
+spec_table_meta(ConjKind::TwistConj,    (ConjTableBase<N_TWIST>),       "TcTwist")
+spec_table_meta(ConjKind::SliceConj,    (ConjTableBase<N_SLICE>),       "TcSlice")
+spec_table_meta(ConjKind::Edge4Conj,    (ConjTableBase<N_EDGE4>),       "TcEdge4")
+spec_table_meta(ConjKind::Edge8Conj,    (ConjTableBase<N_EDGE8>),       "TcEdge8")
+spec_table_meta(ConjKind::CornerConj,   (ConjTableBase<N_CORNER>),      "TcCorner")
+spec_table_meta(ConjKind::FlipSliceConj,(FlipSliceConj),                "TcFlipSlice")
+spec_table_meta(EqKind::RepCorner,      (SymReduction::EqCornerDom),    "TeRepCorner")
+spec_table_meta(EqKind::RepFlipSlice,   (SymReduction::EqFlipSliceDom), "TeRepFlipSlice")
+spec_table_meta(EqKind::EmbCorner,      (SymReduction::CornerDom),      "TeEmbCorner")
+spec_table_meta(EqKind::EmbFlipSlice,   (SymReduction::FlipSliceDom),   "TeEmbFlipSlice")
+spec_table_meta(StabKind::CornerStab,   (SymReduction::CornerStab),     "TsCorner")
+spec_table_meta(StabKind::FlipSliceStab,(SymReduction::FlipSliceStab),  "TsFlipSlice")
+spec_table_meta(PrunKind::SliceFlip,        (PrunTableBase<N_SLICE,N_FLIP>),        "TpSliceFlip")
+spec_table_meta(PrunKind::SliceTwist,       (PrunTableBase<N_SLICE,N_TWIST>),       "TpSliceTwist")
+spec_table_meta(PrunKind::Edge4Edge8,       (PrunTableBase<N_EDGE4,N_EDGE8>),       "TpEdge4Edge8")
+spec_table_meta(PrunKind::Edge4Corner,      (PrunTableBase<N_EDGE4,N_CORNER>),      "TpEdge4Corner")
+spec_table_meta(PrunKind::Edge8EqCorner,    (PrunTableBase<N_EDGE8,EQ_CORNER>),     "TpEdge8EqCorner")
+spec_table_meta(PrunKind::TwistEqFlipSlice, (PrunTableBase<N_TWIST,EQ_FLIPSLICE>),  "TpTwistEqFlipSlice")
+#undef Unwrap
+#undef spec_table_meta
+template<size_t... Is> auto make_mk_tuple(std::index_sequence<Is...>) -> std::tuple<std::unique_ptr<table_t<static_cast<MoveKind>(Is)>>...>;
+template<size_t... Is> auto make_ck_tuple(std::index_sequence<Is...>) -> std::tuple<std::unique_ptr<table_t<static_cast<ConjKind>(Is)>>...>;
+template<size_t... Is> auto make_ek_tuple(std::index_sequence<Is...>) -> std::tuple<std::unique_ptr<table_t<static_cast<EqKind>(Is)>>...>;
+template<size_t... Is> auto make_sk_tuple(std::index_sequence<Is...>) -> std::tuple<std::unique_ptr<table_t<static_cast<StabKind>(Is)>>...>;
+template<size_t... Is> auto make_pk_tuple(std::index_sequence<Is...>) -> std::tuple<std::unique_ptr<table_t<static_cast<PrunKind>(Is)>>...>;
+using MoveTables= decltype(make_mk_tuple(std::make_index_sequence<size_t(MoveKind::Count)>{}));
+using ConjTables= decltype(make_ck_tuple(std::make_index_sequence<size_t(ConjKind::Count)>{}));
+using EqTables  = decltype(make_ek_tuple(std::make_index_sequence<size_t(EqKind::Count)>{}));
+using StabTables= decltype(make_sk_tuple(std::make_index_sequence<size_t(StabKind::Count)>{}));
+using PrunTables= decltype(make_pk_tuple(std::make_index_sequence<size_t(PrunKind::Count)>{}));
+using Tables    = decltype(std::tuple_cat(
+    std::declval<MoveTables>(),
+    std::declval<ConjTables>(),
+    std::declval<EqTables>(),
+    std::declval<StabTables>(),
+    std::declval<PrunTables>()
+));
+template<auto K>
+constexpr size_t index_v = []() constexpr {
+    using T = decltype(K);
+    constexpr size_t C_Move = size_t(MoveKind::Count)
+                    ,C_Conj = size_t(ConjKind::Count)
+                    ,C_Eq   = size_t(EqKind::Count)
+                    ,C_Stab = size_t(StabKind::Count);
+    if constexpr (std::is_same_v<T, MoveKind>)  return size_t(K);
+    if constexpr (std::is_same_v<T, ConjKind>)  return size_t(K) + C_Move;
+    if constexpr (std::is_same_v<T, EqKind>)    return size_t(K) + C_Move + C_Conj;
+    if constexpr (std::is_same_v<T, StabKind>)  return size_t(K) + C_Move + C_Conj + C_Eq;
+    if constexpr (std::is_same_v<T, PrunKind>)  return size_t(K) + C_Move + C_Conj + C_Eq + C_Stab;
+}();
+class TablePool
+{
+    Tables tables_;
+public:
+    static const TablePool& instance() { return instance_mut(); }
+    template<auto K> const auto& get() const
+    {
+        const auto& slot = std::get<index_v<K>>(tables_);
+        if(!slot) {
+            VTRACE("fatal: table %s is not initialized\n", tag_v<K>.data());
+            std::terminate();
+        }
+        return *slot;
+    }
+    template<auto K, typename Builder> const auto& ensure(Builder&& builder)
+    {
+        auto& slot = std::get<index_v<K>>(tables_);
+        if(!slot) {
+            slot = std::make_unique<table_t<K>>();
+            builder(*slot);
+        }
+        return *slot;
+    }
+    template<auto... K> void clean() { (std::get<index_v<K>>(tables_).reset(),...); }
+private:
+    friend class TableProvider;
+    static TablePool& instance_mut() { static TablePool pool; return pool; }
+};
+enum class LoadStatus { Ok, Missing, Incompatible, Corrupted, OsErr };
+template<auto K> struct table_with_header;
+class TableProvider
+{
+    fs::path tdir_;
+    TablePool& pool_;
+public:
+    template<bool use_symmetry>
+    static const TableProvider& make(fs::path dir = {})
+    {
+        static TableProvider tp (
+            dir.empty() ? cube::config::get_table_dir() : dir,
+            std::bool_constant<use_symmetry>()
+        );
+        return tp;
+    }
+    template<auto K> [[nodiscard]] const auto& get() const { return pool_.get<K>(); }
+private:
+    template<bool use_symmetry>
+    TableProvider(fs::path dir, std::bool_constant<use_symmetry>)
+        : tdir_{dir}
+        , pool_{TablePool::instance_mut()}
+    { load_or_build<use_symmetry>(); }
+    template<bool use_symmetry>
+    void load_or_build()
+    {
+        ensure_all<
+            MoveKind::Twist, MoveKind::Flip, MoveKind::Slice,
+            MoveKind::Edge4, MoveKind::Edge8, MoveKind::Corner
+        >();
+        if constexpr (!use_symmetry) {
+            ensure_all<
+                PrunKind::SliceFlip, PrunKind::SliceTwist,
+                PrunKind::Edge4Edge8, PrunKind::Edge4Corner
+            >();
+        } else {
+            ensure_all<
+                EqKind::EmbCorner, EqKind::EmbFlipSlice,
+                ConjKind::TwistConj, ConjKind::Edge8Conj,
+                PrunKind::TwistEqFlipSlice,
+                PrunKind::Edge4Edge8, PrunKind::Edge8EqCorner
+            >();
+            pool_.clean<
+                ConjKind::CornerConj, ConjKind::FlipSliceConj,
+                StabKind::CornerStab, StabKind::FlipSliceStab,
+                EqKind::RepCorner, EqKind::RepFlipSlice
+            >();
+        }
+    }
+    template<auto K>
+    static constexpr bool should_persist()
+    {
+        using T = std::remove_cv_t<decltype(K)>;
+        if constexpr (std::is_same_v<T, MoveKind> || std::is_same_v<T, PrunKind>) return true;
+        else if constexpr (std::is_same_v<T, EqKind>) return K == EqKind::EmbCorner || K == EqKind::EmbFlipSlice;
+        else if constexpr (std::is_same_v<T, ConjKind>) return K == ConjKind::TwistConj || K == ConjKind::Edge8Conj;
+        else return false;
+    }
+    template<auto K> const auto& ensure()
+    {
+        return pool_.ensure<K>([this](table_t<K>& t){
+            auto status = load_from<K>(t,tdir_);
+            if(status == LoadStatus::Ok) return;
+            if(!fs::exists(tdir_)) fs::create_directories(tdir_);
+            if constexpr (std::is_same_v<decltype(K), PrunKind>) { VTRACE("[pdb] building pruning table %s...\n", tag_v<K>.data()); }
+            build_table<K>(t);
+            if constexpr (std::is_same_v<decltype(K), PrunKind>) { VTRACE("[pdb] done.\n"); }
+            if constexpr (should_persist<K>()) save_to<K>(t,tdir_);
+        });
+    }
+    template<auto... Ks> void ensure_all() { (ensure<Ks>(), ...); }
+    template<MoveKind K> void build_table(table_t<K>& t)
+    {
+        if constexpr (K == MoveKind::Twist)   { t.build(transform_twist); }
+        if constexpr (K == MoveKind::Flip)    { t.build(transform_flip); }
+        if constexpr (K == MoveKind::Slice)   { t.build(transform_slice); }
+        if constexpr (K == MoveKind::Edge4)   { t.build(transform_edge4); }
+        if constexpr (K == MoveKind::Edge8)   { t.build(transform_edge8); }
+        if constexpr (K == MoveKind::Corner)  { t.build(transform_corner); }
+    }
+    template<ConjKind K> void build_table(table_t<K>& t)
+    {
+        if constexpr (K == ConjKind::TwistConj)  { t.build(conjugate_twist); }
+        if constexpr (K == ConjKind::SliceConj)  { t.build(conjugate_slice); }
+        if constexpr (K == ConjKind::Edge4Conj)  { t.build(conjugate_edge4); }
+        if constexpr (K == ConjKind::Edge8Conj)  { t.build(conjugate_edge8); }
+        if constexpr (K == ConjKind::CornerConj) { t.build(conjugate_corner); }
+        if constexpr (K == ConjKind::FlipSliceConj) {
+            for(int flip = 0; flip < N_FLIP; ++flip) {
+                for(int slice = 0; slice < N_SLICE; ++slice) {
+                    for(auto s: sym_entries)
+                        t[s][flip][slice] = conjugate_flipslice(s, {uint16_t(flip),uint16_t(slice)});
+                }
+            }
+        }
+    }
+    template<StabKind K> void build_table(table_t<K>& t)
+    {
+        if constexpr(K == StabKind::CornerStab) {
+            SymReduction::build_stab_corner(t,
+                ensure<EqKind::RepCorner>(), ensure<ConjKind::CornerConj>()
+            );
+        }
+        if constexpr(K == StabKind::FlipSliceStab) {
+            SymReduction::build_stab_flipslice(t,
+                ensure<EqKind::RepFlipSlice>(), ensure<ConjKind::FlipSliceConj>()
+            );
+        }
+    }
+    template<EqKind K> void build_table(table_t<K>& t)
+    {
+        if constexpr (K == EqKind::EmbCorner)   { SymReduction::build_emb_corner(t, ensure<ConjKind::CornerConj>()); }
+        if constexpr (K == EqKind::RepCorner)   { SymReduction::build_rep_corner(t, ensure<EqKind::EmbCorner>()); }
+        if constexpr (K == EqKind::EmbFlipSlice){ SymReduction::build_emb_flipslice(t, ensure<ConjKind::FlipSliceConj>()); }
+        if constexpr (K == EqKind::RepFlipSlice){ SymReduction::build_rep_flipslice(t, ensure<EqKind::EmbFlipSlice>()); }
+    }
+    template<PrunKind K> void build_table(table_t<K>& t)
+    {
+        if constexpr (K == PrunKind::SliceTwist)    { t.build_disjU(ensure<MoveKind::Slice>(), ensure<MoveKind::Twist>()); return; }
+        if constexpr (K == PrunKind::SliceFlip)     { t.build_disjU(ensure<MoveKind::Slice>(), ensure<MoveKind::Flip>()); return ; }
+        if constexpr (K == PrunKind::Edge4Corner)   { t.build_disjU(ensure<MoveKind::Edge4>(), ensure<MoveKind::Corner>()); return; }
+        if constexpr (K == PrunKind::Edge4Edge8)    { t.build_disjU(ensure<MoveKind::Edge4>(), ensure<MoveKind::Edge8>()); return; }
+        if constexpr (K == PrunKind::Edge8EqCorner) {
+            t.build_XxEqY(
+                ensure<MoveKind::Edge8>(), ensure<ConjKind::Edge8Conj>(),
+                ensure<MoveKind::Corner>(), ensure<EqKind::EmbCorner>(), ensure<EqKind::RepCorner>(), ensure<StabKind::CornerStab>()
+            );
+        }
+        if constexpr (K == PrunKind::TwistEqFlipSlice) {
+            const auto& tran_flip = ensure<MoveKind::Flip>();
+            const auto& tran_slice = ensure<MoveKind::Slice>();
+            auto tran_flipslice = [&tran_flip,&tran_slice](TurnMove m, const flipslice_t& fs) -> flipslice_t {
+                return { tran_flip(m,fs[0]), tran_slice(m,fs[1]) };
+            };
+            t.build_XxEqY(
+                ensure<MoveKind::Twist>(), ensure<ConjKind::TwistConj>(),
+                tran_flipslice,
+                ensure<EqKind::EmbFlipSlice>(), ensure<EqKind::RepFlipSlice>(), ensure<StabKind::FlipSliceStab>()
+            );
+        }
+    }
+    template<auto K>
+    static bool save_to(const table_t<K>& table, const fs::path& dir)
+    {
+        static_assert(std::is_trivially_copyable_v<table_t<K>>);
+        std::ofstream f(dir / tag_v<K>, std::ios::binary);
+        if(!f) return false;
+        return table_with_header<K>::write(f, table);
+    }
+    template <auto K>
+    static auto load_from(table_t<K>& table, const fs::path& dir) -> LoadStatus
+    {
+        static_assert(std::is_trivially_copyable_v<table_t<K>>);
+        std::ifstream f(dir / tag_v<K>, std::ios::binary);
+        if(!f) return LoadStatus::Missing;
+        return table_with_header<K>::read(f, table);
+    }
+};
+template<auto K>
+struct table_with_header
+{
+    struct meta_info { uint64_t filetag, checksum; };
+    static constexpr auto encode_filetag(std::array<uint32_t,2> vk) -> uint64_t
+    {
+        const auto& [ver,kind] = vk;
+        return TABLE_MAGIC ^ ((uint64_t(ver) << 32) | uint64_t(kind));
+    }
+    [[maybe_unused]]
+    static auto decode_filetag(uint64_t tag) -> std::array<uint32_t,2>
+    {
+        const uint64_t vk = tag ^ TABLE_MAGIC;
+        return { uint32_t(vk >> 32), uint32_t(vk & 0xffffffff) };
+    }
+    static constexpr uint64_t filetag_v = encode_filetag({uint32_t(TABLE_VERSION),uint32_t(index_v<K>)});
+    static auto read(std::ifstream& is, table_t<K>& t) -> LoadStatus
+    {
+        meta_info header{};
+        if(!is.read(reinterpret_cast<char*>(&header), sizeof(header))) return LoadStatus::OsErr;
+        if(!(header.filetag == filetag_v)) return LoadStatus::Incompatible;
+        if(!is.read(reinterpret_cast<char*>(&t.storage), sizeof(t.storage))) return LoadStatus::OsErr;
+        if(fnv1a(&t.storage, sizeof(t.storage)) != header.checksum) return LoadStatus::Corrupted;
+        return LoadStatus::Ok;
+    }
+    static auto write(std::ofstream& os, const table_t<K>& t) -> bool
+    {
+        const auto header = meta_info{filetag_v, fnv1a(&t.storage, sizeof(t.storage))};
+        os.write(reinterpret_cast<const char*>(&header), sizeof(header));
+        os.write(reinterpret_cast<const char*>(&t.storage), sizeof(t.storage));
+        return bool(os);
+    }
+private:
+    static uint64_t fnv1a(const void* data, size_t n)
+    {
+        const auto* p = static_cast<const uint8_t*>(data);
+        uint64_t h = 1469598103934665603ull;
+        for(size_t i = 0; i < n; ++i) { h ^= p[i]; h *= 1099511628211ull; }
+        return h;
+    }
+};
+}
+namespace cube::pdb::ops {
+using namespace cube::data;
+using namespace cube::pdb::table;
+using cube::config::use_symmetry;
+enum Phase { Ph1=0, Ph2=1 };
+struct CoordOps
+{
+    template<Phase PhX>
+    static auto transform(const Coord&, TurnMove) -> Coord;
+    template<Phase PhX>
+    static auto distance(const Coord&) -> uint8_t;
+    template<Phase PhX>
+    static auto origin(const CubieCube& cc, const std::vector<TurnMove>& sol = {}) -> Coord;
+};
+template<Phase PhX>
+auto CoordOps::transform(const Coord& c, TurnMove m) -> Coord
+{
+    const TablePool& pool = TablePool::instance();
+    if constexpr (PhX == Ph1)
+        return {
+            pool.get<MoveKind::Twist>()(m,c.twist),
+            pool.get<MoveKind::Flip>()(m,c.flip),
+            pool.get<MoveKind::Slice>()(m,c.slice),
+            -1,-1,-1
+        };
+    else
+        return {
+            0,0,0,
+            pool.get<MoveKind::Corner>()(m,c.corner),
+            pool.get<MoveKind::Edge4>()(m,c.edge4),
+            pool.get<MoveKind::Edge8>()(m,c.edge8)
+        };
+}
+template<Phase PhX>
+auto CoordOps::distance(const Coord& c) -> uint8_t
+{
+    const TablePool& pool = TablePool::instance();
+    if constexpr (PhX == Ph1) {
+        if (!use_symmetry()) {
+            return std::max(
+                pool.get<PrunKind::SliceTwist>()(c.slice,c.twist),
+                pool.get<PrunKind::SliceFlip>()(c.slice,c.flip)
+            );
+        } else {
+            const auto [eq, s] = pool.get<EqKind::EmbFlipSlice>()(c.flip,c.slice);
+            return pool.get<PrunKind::TwistEqFlipSlice>()(
+                pool.get<ConjKind::TwistConj>()(~s,c.twist),
+                eq
+            );
+        }
+    } else {
+        if (!use_symmetry()) {
+            return std::max(
+                pool.get<PrunKind::Edge4Corner>()(c.edge4,c.corner),
+                pool.get<PrunKind::Edge4Edge8>()(c.edge4,c.edge8)
+            );
+        } else {
+            const auto [eq, s] = pool.get<EqKind::EmbCorner>()(c.corner);
+            return std::max(
+                pool.get<PrunKind::Edge4Edge8>()(c.edge4,c.edge8),
+                pool.get<PrunKind::Edge8EqCorner>()(
+                    pool.get<ConjKind::Edge8Conj>()(~s,c.edge8),
+                    eq
+                )
+            );
+        }
+    }
+}
+template<Phase PhX>
+auto CoordOps::origin(const CubieCube& cc, const std::vector<TurnMove>& sol) -> Coord
+{
+    if constexpr (PhX == Ph1) return Coord::CubieCube2Coord(cc);
+    else {
+        const TablePool& pool = TablePool::instance();
+        Coord c = Coord::CubieCube2Coord(cc);
+        int corner = c.corner, edge4, edge8;
+        auto ep = cc.ep;
+        for(auto m: sol)
+        {
+            corner = pool.get<MoveKind::Corner>()(m,corner);
+            ep = ep * ElementaryMove[m].ep;
+        }
+        edge4 = Coord::ep2edge4(ep);
+        edge8 = Coord::ep2edge8(ep);
+        return { 0,0,0,corner,edge4,edge8 };
+    }
+}
+}
+namespace cube::solver {
+using namespace cube::data;
+using namespace cube::pdb;
+using namespace cube::pdb::table;
+using cube::pdb::ops::Phase;
+class SearchContext
+{
+public:
+    SearchContext(const CubieCube& cc = CubieCube::id, size_t good = 22)
+        : root_(cc), tgtlen_(good)
+    {}
+    CubieCube   root() const    { return root_; }
+    size_t      tgtlen() const  { return tgtlen_; }
+    template<Phase PhX>
+    void reset() { path_[PhX].fill(-1); }
+    template<Phase PhX> void record(int step, int move){ path_[PhX][step] = move; }
+    template<Phase PhX> int  recall(int step) const    { return path_[PhX][step]; }
+    template<Phase PhX>
+    void commit(size_t L)
+    {
+        rsolution_[PhX].first = L;
+        std::copy(path_[PhX].begin(), path_[PhX].begin()+L, rsolution_[PhX].second.begin());
+    }
+    template<Phase PhX>
+    auto extract() const -> std::vector<TurnMove>
+    {
+        size_t n = rsolution_[PhX].first;
+        if(n == 0) return {};
+        std::vector<TurnMove> sol(n);
+        for(auto i = 0; i < n; i++) {
+            sol[i] = static_cast<TurnMove>(rsolution_[PhX].second[n-1-i]);
+        }
+        return sol;
+    }
+    static constexpr int D0 = 12, D1 = 18, DS = D0+D1;
+    template<Phase PhX> static constexpr auto D = std::get<PhX>(std::tie(D0,D1));
+    static constexpr std::array<TurnMove,18> EM0
+        = { Ux1,Ux2,Ux3,Rx1,Rx2,Rx3,Fx1,Fx2,Fx3,Dx1,Dx2,Dx3,Lx1,Lx2,Lx3,Bx1,Bx2,Bx3 };
+    static constexpr std::array<TurnMove,10> EM1
+        = { Ux1,Ux2,Ux3,Rx2,Fx2,Dx1,Dx2,Dx3,Lx2,Bx2 };
+    template<Phase PhX>
+    static constexpr auto& EM = std::get<PhX>(std::tie(EM0,EM1));
+private:
+    CubieCube root_;
+    size_t tgtlen_;
+    std::array<std::array<int,DS+2>,2>                  path_;
+    std::array<std::pair<size_t,std::array<int,DS>>,2>  rsolution_;
+};
+class KociembaSolver
+{
+public:
+    static auto solve(const CubieCube &cc, size_t good)
+        -> std::tuple<bool,std::vector<TurnMove>,std::vector<TurnMove>>;
+protected:
+    static int start_search_ph1(SearchContext& ctx, const Coord& c, size_t d, size_t d_orig, size_t maxL);
+    static bool start_search_ph2(SearchContext& ctx, const Coord& c, size_t togo);
+};
+}
+namespace cube::solver {
+using namespace cube::pdb::ops;
+inline bool is_dull_pair(const TurnMove A, const int B)
+{
+    if(!(B >= Ux1 && B <= Bx3)) return false;
+    return (A/3 == B/3) || (A/3 == B/3+3);
+}
+auto KociembaSolver::solve(const CubieCube &cc, size_t good)
+    -> std::tuple<bool,std::vector<TurnMove>,std::vector<TurnMove>>
+{
+    size_t bestL = SearchContext::DS + 1;
+    auto ctx = SearchContext(cc,good);
+    ctx.reset<Ph1>();
+    auto c = CoordOps::origin<Ph1>(cc);
+    for(auto togo = CoordOps::distance<Ph1>(c); togo < bestL; ++togo)
+    {
+        int l = start_search_ph1(ctx, c,togo,togo,bestL-1);
+        if(l >= 0) bestL = std::min(bestL,size_t(l));
+        if(bestL <= ctx.tgtlen()) break;
+    }
+    if(bestL > SearchContext::DS) return {false, {}, {}};
+    else return { true, ctx.extract<Ph1>(), ctx.extract<Ph2>() };
+}
+int KociembaSolver::start_search_ph1(
+    SearchContext& ctx, const Coord& c, size_t d, size_t d_orig, size_t maxL)
+{
+    if(d < CoordOps::distance<Ph1>(c)) return -1;
+    if(d == 0) {
+        ctx.commit<Ph1>(d_orig);
+        ctx.reset<Ph2>();
+        auto c2 = CoordOps::origin<Ph2>(ctx.root(), ctx.extract<Ph1>());
+        for(auto togo = CoordOps::distance<Ph2>(c2); togo <= maxL; ++togo)
+        {
+            bool succ = start_search_ph2(ctx,c2,togo);
+            if(succ) {
+                ctx.commit<Ph2>(togo);
+                return togo;
+            }
+        }
+        return -1;
+    }
+    int bestL = maxL + 1;
+    for(auto m: SearchContext::EM<Ph1>)
+    {
+        if(is_dull_pair(m, ctx.recall<Ph1>(d))) continue;
+        ctx.record<Ph1>(d-1,m);
+        auto l = start_search_ph1(ctx, CoordOps::transform<Ph1>(c,m), d-1, d_orig, bestL-2);
+        if(l >= 0 && bestL > l+1) {
+            bestL = l+1;
+        }
+        if(bestL + d_orig - d <= ctx.tgtlen()) break;
+        if(d >= bestL) break;
+    }
+    return bestL > int(maxL) ? -1 : bestL;
+}
+bool KociembaSolver::start_search_ph2(
+    SearchContext& ctx, const Coord& c, size_t togo)
+{   if(togo < CoordOps::distance<Ph2>(c)) return false;
+    if(togo == 0) return CoordOps::distance<Ph2>(c) == 0;
+    for(auto m: SearchContext::EM<Ph2>)
+    {
+        if(is_dull_pair(m, ctx.recall<Ph2>(togo))) continue;
+        ctx.record<Ph2>(togo-1,m);
+        bool ret = start_search_ph2(ctx, CoordOps::transform<Ph2>(c,m), togo-1);
+        if(ret) return true;
+    }
+    return false;
+}
+}
+namespace cube::config
+{
+namespace fs = std::filesystem;
+namespace {
+bool& use_symmetry_ref()
+{
+    static bool val_ = CUBE_USE_SYMMETRY;
+    return val_;
+}
+fs::path& get_custom_dir_storage()
+{
+    static fs::path custom_dir = {};
+    return custom_dir;
+}
+auto get_cache_dir() -> std::filesystem::path
+{
+#ifdef _WIN32
+    const char* localAppData = std::getenv("LOCALAPPDATA");
+    if (localAppData) return std::filesystem::path(localAppData);
+#elif __APPLE__
+    const char* home = std::getenv("HOME");
+    if (home) return std::filesystem::path(home) / "Library" / "Caches";
+#else
+    const char* xdgCache = std::getenv("XDG_CACHE_HOME");
+    if (xdgCache) return std::filesystem::path(xdgCache);
+    const char* home = std::getenv("HOME");
+    if (home) return std::filesystem::path(home) / ".cache";
+#endif
+    throw std::runtime_error("Unable to determine cache path");
+}
+}
+bool use_symmetry() { return use_symmetry_ref(); }
+void use_symmetry(bool v)
+{
+    static std::once_flag once;
+    std::call_once(once, [v]() { use_symmetry_ref() = v; });
+}
+void set_table_dir(fs::path dir)
+{
+    static std::once_flag once;
+    std::call_once(once, [dir]() { get_custom_dir_storage() = dir; });
+}
+fs::path get_table_dir()
+{
+    const auto user_dir = get_custom_dir_storage();
+    if(!user_dir.empty()) return user_dir;
+    if(const char *env = std::getenv("CUBE_TABLE_DIR")) {
+        if(env[0] != '\0') return fs::path(env);
+    }
+    try {
+        return get_cache_dir() / "cube.coshz" / "tables";
+    } catch (...) {
+        return fs::absolute("tables");
+    }
+}
+}
+namespace cube::internal
+{
+using namespace cube::data;
+std::string apply_maneuver(std::string_view s, const std::vector<TurnMove>& ms);
+auto parse_maneuver(std::string_view s) -> std::vector<TurnMove>;
+inline auto operator""_Tm(const char* ts, size_t n) -> std::vector<TurnMove>
+{
+    return parse_maneuver(std::string(ts,n));
+}
+bool is_valid_maneuver(std::string_view);
+}
+namespace cube {
+using namespace cube::data;
+namespace internal {
+std::string apply_maneuver(std::string_view s, const std::vector<TurnMove> &ms)
+{
+    assert(s.size() == 54 && "invalid cube length");
+    char buf[2][54];
+    std::memcpy(buf[0], s.data(), 54);
+    size_t curr = 0;
+    for(const auto &m : ms)
+    {
+        const auto& p   = ElementaryPerm[m].f;
+        const char* src = buf[curr];
+        char*       dst = buf[1-curr];
+        for(int i = 0; i < 54; ++i) dst[i] = src[p[i]];
+        curr = 1-curr;
+    }
+    return std::string(buf[curr],54);
+}
+auto parse_maneuver(std::string_view s) -> std::vector<TurnMove>
+{
+    assert(is_valid_maneuver(s) && "invalid maneuver");
+    auto expand = [](std::string_view in) -> std::string {
+        static const std::regex group_re(R"(\(([^)]+)\)(?:\{(\d+)\})?)");
+        std::string res;
+        auto start = in.cbegin();
+        std::match_results<std::string_view::const_iterator> m;
+        while (std::regex_search(start, in.cend(), m, group_re)) {
+            res.append(start, m[0].first);
+            int repeat = m[2].matched ? std::stoi(m[2].str()) : 1;
+            for (int i = 0; i < repeat; ++i) res += m[1].str();
+            start = m[0].second;
+        }
+        res.append(start, in.cend());
+        return res;
+    };
+    auto char_to_move = [](char c) -> TurnMove {
+        switch(c) {
+        case 'U': return Ux1;
+        case 'R': return Rx1;
+        case 'F': return Fx1;
+        case 'D': return Dx1;
+        case 'L': return Lx1;
+        case 'B': return Bx1;
+        default: throw std::invalid_argument("char_to_move: ???");
+        }
+    };
+    std::string in = expand(s);
+    std::vector<TurnMove> ms;
+    ms.reserve(in.size());
+    for (char c : in)
+    {
+        switch (c) {
+        case ' ': break;
+        case '2':
+            ms.back() = static_cast<TurnMove>(ms.back() + 1);
+            break;
+        case '\'':
+            ms.back() = static_cast<TurnMove>(ms.back() + 2);
+            break;
+        default:
+            ms.push_back(char_to_move(c));
+            break;
+        }
+    }
+    return ms;
+}
+bool is_valid_maneuver(std::string_view s)
+{
+    static const std::regex pat(
+        R"(\s*(([UDLRFB]['23]?|\(([UDLRFB]['23]?\s*)+\)(\{\d+\})?)\s*)*)"
+    );
+    return std::regex_match(s.begin(), s.end(), pat);
+}
+}
+}
+namespace cube::symmetry {
+void SymReduction::build_emb_corner(CornerDom& emb, const CornerConj& conj)
+{
+    static_assert(EQ_CORNER <= std::numeric_limits<uint16_t>::max());
+    constexpr auto unvisited = corner_eq_t { std::numeric_limits<uint16_t>::max(),Id };
+    emb.fill(unvisited);
+    for(int c = 0, eq = -1; c < N_CORNER; ++c)
+    {
+        if(emb[c].eq != unvisited.eq) continue;
+        emb[c] = { uint16_t(++eq), Id };
+        for(Sym s: sym_entries)
+        {
+            if(s == Id) continue;
+            auto &entry = emb[conj(s,c)];
+            if(entry.eq == unvisited.eq) entry = { uint16_t(eq), s };
+        }
+    }
+}
+void SymReduction::build_rep_corner(EqCornerDom& rep, const CornerDom& emb)
+{
+    const auto unvisited = std::numeric_limits<uint16_t>::max();
+    rep.fill(unvisited);
+    for(int corner = 0; corner < N_CORNER; ++corner)
+    {
+        auto eq = emb[corner].eq;
+        if(rep[eq] == unvisited) rep[eq] = corner;
+    }
+}
+void SymReduction::build_emb_flipslice(FlipSliceDom& emb, const FlipSliceConj& conj)
+{
+    static_assert(EQ_FLIPSLICE <= std::numeric_limits<uint16_t>::max());
+    constexpr auto unvisited = flipslice_eq_t { std::numeric_limits<uint16_t>::max(),Id };
+    emb.fill(unvisited);
+    for(int flip = 0, eq = -1; flip < N_FLIP; ++flip)
+        for(int slice = 0; slice < N_SLICE; ++slice)
+    {
+        if(emb(flip,slice).eq != unvisited.eq) continue;
+        emb(flip,slice) = { uint16_t(++eq), Id };
+        for(auto s: sym_entries)
+        {
+            if(s == Id) continue;
+            auto fs = conj(s,flip,slice);
+            auto &entry = emb(fs[0],fs[1]);
+            if(entry.eq == unvisited.eq) entry = { uint16_t(eq), s };
+        }
+    }
+}
+void SymReduction::build_rep_flipslice(EqFlipSliceDom& rep, const FlipSliceDom& emb)
+{
+    const auto unvisited = flipslice_t { std::numeric_limits<uint16_t>::max(),std::numeric_limits<uint16_t>::max() };
+    rep.fill(unvisited);
+    for(int flip = 0; flip < N_FLIP; ++flip)
+        for(int slice = 0; slice < N_SLICE; ++slice)
+        {
+            auto eq = emb(flip,slice).eq;
+            if(rep[eq] == unvisited) rep[eq] = { uint16_t(flip), uint16_t(slice) };
+        }
+}
+void SymReduction::build_stab_corner(CornerStab& stab, const EqCornerDom& rep, const CornerConj& conj)
+{
+    for (int eq = 0; eq < EQ_CORNER; ++eq)
+    {
+        SymMask mask = 0;
+        const auto c = rep[eq];
+        for (Sym s : sym_entries)
+        {
+            if (conj[s][c] == c) mask |= SymMask(1u << s);
+        }
+        stab[eq] = mask;
+    }
+}
+void SymReduction::build_stab_flipslice(FlipSliceStab& stab, const EqFlipSliceDom& rep, const FlipSliceConj& conj)
+{
+    for (int eq = 0; eq < EQ_FLIPSLICE; ++eq)
+    {
+        SymMask mask = 0;
+        const auto fs = rep[eq];
+        for (Sym s : sym_entries)
+        {
+            if (conj(s,fs[0],fs[1]) == fs) mask |= SymMask(1u << s);
+        }
+        stab[eq] = mask;
+    }
+}
+}
+#ifndef CUBE_EXPORT_H
+#define CUBE_EXPORT_H
+#ifdef CUBE_STATIC_DEFINE
+#  define CUBE_EXPORT
+#  define CUBE_NO_EXPORT
+#else
+#  ifndef CUBE_EXPORT
+#    ifdef cube_shared_EXPORTS
+#      define CUBE_EXPORT __attribute__((visibility("default")))
+#    else
+#      define CUBE_EXPORT __attribute__((visibility("default")))
+#    endif
+#  endif
+#  ifndef CUBE_NO_EXPORT
+#    define CUBE_NO_EXPORT __attribute__((visibility("hidden")))
+#  endif
+#endif
+#ifndef CUBE_DEPRECATED
+#  define CUBE_DEPRECATED __attribute__ ((__deprecated__))
+#endif
+#ifndef CUBE_DEPRECATED_EXPORT
+#  define CUBE_DEPRECATED_EXPORT CUBE_EXPORT CUBE_DEPRECATED
+#endif
+#ifndef CUBE_DEPRECATED_NO_EXPORT
+#  define CUBE_DEPRECATED_NO_EXPORT CUBE_NO_EXPORT CUBE_DEPRECATED
+#endif
+#if 0
+#  ifndef CUBE_NO_DEPRECATED
+#    define CUBE_NO_DEPRECATED
+#  endif
+#endif
+#endif
+#define CUBE_VERSION        "0.5.1"
+#define CUBE_VERSION_FULL   "0.5.1"
+#define CUBE_VERSION_MAJOR  0
+#define CUBE_VERSION_MINOR  5
+#define CUBE_VERSION_PATCH  1
+#define CUBE_VER_BIT_MAJOR_ 5
+#define CUBE_VER_BIT_MINOR_ 7
+#define CUBE_VER_BIT_PATCH_ 4
+#define CUBE_VER_SFT_MAJOR_ (CUBE_VER_BIT_MINOR_ + CUBE_VER_BIT_PATCH_)
+#define CUBE_VER_SFT_MINOR_ CUBE_VER_BIT_PATCH_
+#define CUBE_VER_SFT_PATCH_ 0
+#define CUBE_VER_MSK_MAJOR_ ((1U << CUBE_VER_BIT_MAJOR_) - 1)
+#define CUBE_VER_MSK_MINOR_ ((1U << CUBE_VER_BIT_MINOR_) - 1)
+#define CUBE_VER_MSK_PATCH_ ((1U << CUBE_VER_BIT_PATCH_) - 1)
+#if 0 > CUBE_VERSION_MAJOR || CUBE_VERSION_MAJOR > CUBE_VER_MSK_MAJOR_
+#   error "CUBE_VERSION_MAJOR out of range [0..31]"
+#endif
+#if 0 > CUBE_VERSION_MINOR || CUBE_VERSION_MINOR > CUBE_VER_MSK_MINOR_
+#   error "CUBE_VERSION_MINOR out of range [0..127]"
+#endif
+#if 0 > CUBE_VERSION_PATCH || CUBE_VERSION_PATCH > CUBE_VER_MSK_PATCH_
+#   error "CUBE_VERSION_PATCH out of range [0..15]"
+#endif
+#define CUBE_VERSION_CODE (\
+    ((CUBE_VERSION_MAJOR & CUBE_VER_MSK_MAJOR_) << CUBE_VER_SFT_MAJOR_) | \
+    ((CUBE_VERSION_MINOR & CUBE_VER_MSK_MINOR_) << CUBE_VER_SFT_MINOR_) | \
+    ((CUBE_VERSION_PATCH & CUBE_VER_MSK_PATCH_) << CUBE_VER_SFT_PATCH_) )
+#ifndef __cplusplus
+#error "This is a C++ header; please use a C++ compiler OR include cube/cube.h instead"
+#endif
+namespace cube {
+constexpr std::string_view cubeId = "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB";
+enum class SolveResult : int32_t {
+    Success       = 0,
+    Unsolvable    = 1,
+    NotFound      = 2,
+    InvalidSrc    = 3,
+    InvalidTgt    = 4,
+    UnknownErr    = 5
+};
+CUBE_EXPORT constexpr std::string_view to_string(SolveResult status) noexcept {
+    switch (status) {
+    case SolveResult::Success:      return "Success";
+    case SolveResult::Unsolvable:   return "The cube configuration is unsolvable";
+    case SolveResult::NotFound:     return "No solution found within the step limit";
+    case SolveResult::InvalidSrc:   return "Invalid source color configuration";
+    case SolveResult::InvalidTgt:   return "Invalid target color configuration";
+    case SolveResult::UnknownErr:   return "Unknown error";
+    }
+}
+enum class PermFormat : int {
+    Face  = 0,
+    Cubie = 1,
+    Cycle = 2
+};
+struct Solution {
+    SolveResult status;
+    std::string maneuver;
+    bool is_success() const noexcept
+    {
+        return status == SolveResult::Success;
+    }
+};
+CUBE_EXPORT bool is_solvable(std::string_view color_cube);
+[[nodiscard]] CUBE_EXPORT Solution solve(
+    std::string_view src,
+    std::string_view tgt = cubeId,
+    int good = 22
+);
+[[nodiscard]] CUBE_EXPORT std::string apply_maneuver(
+    std::string_view maneuver,
+    std::string_view cube = cubeId
+);
+[[nodiscard]] CUBE_EXPORT std::string show_permutation(
+    std::string_view ms_or_cube,
+    PermFormat format = PermFormat::Cycle
+);
+}
+#ifndef CUBE_USE_SYMMETRY
+#   define CUBE_USE_SYMMETRY 1
+#endif
+namespace cube {
+CUBE_EXPORT void cube_init(
+    bool use_symmetry = CUBE_USE_SYMMETRY,
+    std::string_view dir = {}
+);
+CUBE_EXPORT void set_table_dir(std::filesystem::path dir);
+CUBE_EXPORT auto get_table_dir() -> std::filesystem::path;
+CUBE_EXPORT bool is_valid_config(std::string_view cube);
+CUBE_EXPORT bool is_valid_maneuver(std::string_view maneuver);
+}
+namespace cube::show {
+using std::size_t;
+using namespace cube::data;
+template<size_t N>
+using StringArr = std::array<std::string_view,N>;
+inline constexpr StringArr< 8> CornerToString       = { "urf", "ufl", "ulb", "ubr", "dfr", "dlf", "dbl", "drb" };
+inline constexpr StringArr<12> EdgeToString         = { "ur","uf","ul","ub","dr","df","dl","db","fr","fl","bl","br" };
+inline constexpr StringArr< 6> CenterToString       = { "u", "r", "f", "d", "l", "b" };
+inline constexpr StringArr< 3> OrientationToString  = { "", "+", "-" };
+inline constexpr StringArr<18> Move2Str             = { "U","U2","U'","R","R2","R'","F","F2","F'","D","D2","D'","L","L2","L'","B","B2","B'" };
+inline constexpr StringArr<54> Face2Str             = { "U1","U2","U3","U4","U5","U6","U7","U8","U9","R1","R2","R3","R4","R5","R6","R7","R8","R9","F1","F2","F3","F4","F5","F6","F7","F8","F9","D1","D2","D3","D4","D5","D6","D7","D8","D9","L1","L2","L3","L4","L5","L6","L7","L8","L9","B1","B2","B3","B4","B5","B6","B7","B8","B9" };
+inline constexpr std::string_view ColorSet          = "URFDLB";
+inline constexpr std::string_view CornerSet         = "ABCDEFGH";
+inline constexpr std::string_view EdgeSet           = "opqrstuvwxyz";
+enum class CubeFormat {
+    Face,
+    Cubie,
+    Cycle
+};
+template<typename Array, typename Formatter>
+inline std::string seq2str_fmt(const Array &xs,
+                           Formatter && fmt,
+                           std::string_view sep="",
+                           std::string_view pre="",
+                           std::string_view suf="")
+{
+    const size_t len = xs.size();
+    if(len == 0) { return std::string(pre) + std::string(suf); }
+    std::stringstream ss;
+    ss << pre << fmt(xs[0]);
+    for(size_t i = 1; i < len; ++i) ss << sep << fmt(xs[i]);
+    ss << suf;
+    return ss.str();
+}
+template<typename Container>
+inline std::string seq2str(const Container &xs,
+                           std::string_view sep="",
+                           std::string_view pre="",
+                           std::string_view suf="")
+{
+    auto default_formatter = [](const auto& val) {
+        using T = std::decay_t<decltype(val)>;
+        if constexpr (std::is_same_v<T,uint8_t> || std::is_same_v<T,int8_t>) {
+            return int(val);
+        } else {
+            return val;
+        }
+    };
+    return seq2str_fmt(xs, default_formatter, sep, pre, suf);
+}
+inline std::string to_string(const ColorState& cs)
+{
+    return seq2str_fmt(cs.s, [](auto v) {
+        switch(v){
+        case U: return 'U';
+        case R: return 'R';
+        case F: return 'F';
+        case D: return 'D';
+        case L: return 'L';
+        case B: return 'B';
+        default: throw std::invalid_argument("to_string(ColorState): ???");
+        }
+    });
+}
+inline std::string to_string(const FaceCube& fc, bool use_digit=true)
+{
+    if(use_digit){
+        return seq2str(fc.f);
+    } else {
+        return seq2str_fmt(fc.f, [](auto v) { return Face2Str[v]; });
+    }
+}
+inline std::string to_string(const CubieCube& cc, CubeFormat fmt)
+{
+    switch(fmt){
+    case CubeFormat::Face:
+        return to_string(cc.toFaceCube(), false);
+    case CubeFormat::Cubie:
+        return seq2str_fmt(cc.cp, [](auto v) { return CornerSet[v]; }) +
+               seq2str_fmt(cc.co, [](auto v) { return char('0'+v); }) +
+               seq2str_fmt(cc.ep, [](auto v) { return EdgeSet[v]; }) +
+               seq2str_fmt(cc.eo, [](auto v) { return char('0'+v);});
+    default:
+        const auto [fixed_corner,cycles_corner] = decomposite(cc.cp);
+        const auto [fixed_edge, cycles_edge ]   = decomposite(cc.ep);
+        std::string s1, s2;
+        s1.reserve(64);
+        s2.reserve(64);
+        auto append_fixed = [&](std::string& out, const auto& fixed, const auto& ori_arr, auto name_table) {
+            for(auto idx: fixed) {
+                if(ori_arr[idx] == 0) continue;
+                out += '(';
+                out += OrientationToString[ori_arr[idx]];
+                out += name_table[idx];
+                out += ')';
+            }
+        };
+        auto append_cycles = [&](std::string& out, const auto& cycles, const auto& ori_arr, auto name_table) {
+            for(const auto& cycle: cycles) {
+                const size_t N = cycle.size();
+                if(N == 0) continue;
+                out += '(';
+                for(int i = 0; i < N; i++) {
+                    if(i > 0) out += ',';
+                    size_t prev_idx = cycle[(N-1+i)%N];
+                    out += OrientationToString[ori_arr[prev_idx]];
+                    out += name_table[cycle[i]];
+                }
+                out += ')';
+            }
+        };
+        append_fixed(s1, fixed_corner, cc.co, CornerToString);
+        append_cycles(s1, cycles_corner, cc.co, CornerToString);
+        append_fixed(s2, fixed_edge, cc.eo, EdgeToString);
+        append_cycles(s2, cycles_edge, cc.eo, EdgeToString);
+        return s1.empty() && s2.empty() ? "id" : s1 + s2;
+    }
+}
+inline std::string to_string(const std::vector<TurnMove>& ms)
+{
+    return seq2str_fmt(ms, [](auto v){
+        return Move2Str[v];
+    }, " ");
+}
+}
+namespace cube {
+using namespace cube::data;
+using namespace cube::pdb;
+using namespace cube::solver;
+namespace fs = std::filesystem;
+void set_table_dir(fs::path dir) { config::set_table_dir(dir); }
+auto get_table_dir() -> fs::path { return config::get_table_dir(); }
+bool is_valid_config(std::string_view cube) { return ColorState::is_valid_config(cube); }
+bool is_valid_maneuver(std::string_view maneuver) { return internal::is_valid_maneuver(maneuver); }
+void cube_init(bool use_symmetry, std::string_view dir)
+{
+    static std::once_flag once;
+    std::call_once(once, [=]() {
+        config::use_symmetry(use_symmetry);
+        if(!dir.empty()) config::set_table_dir(dir);
+        if(use_symmetry) table::TableProvider::make<true>();
+        else table::TableProvider::make<false>();
+    });
+}
+Solution solve(std::string_view src, std::string_view tgt, int good)
+{
+    if (src != cubeId && !is_valid_config(src)) return {SolveResult::InvalidSrc, ""};
+    if (tgt != cubeId && !is_valid_config(tgt)) return {SolveResult::InvalidTgt, ""};
+    if (src == tgt) return {SolveResult::Success, ""};
+    auto cc_src = ColorState::fromString(src).toCubieCube();
+    auto cc_tgt = ColorState::fromString(tgt).toCubieCube();
+    CubieCube cc = ~cc_tgt * cc_src;
+    if (!cc.isSolvable()) return {SolveResult::Unsolvable, ""};
+    cube_init(CUBE_USE_SYMMETRY);
+    const auto& [found, s1, s2] = KociembaSolver::solve(cc, good < 0 ? 0 : good);
+    if (!found) return {SolveResult::NotFound, ""};
+    std::vector<TurnMove> sol;
+    size_t n1 = s1.size(), n2 = s2.size();
+    if (!s1.empty() && !s2.empty() && s1[n1 - 1] / 3 == s2[0] / 3) {
+        sol.insert(sol.end(), s1.begin(), s1.end() - 1);
+        int m = (s1[n1 - 1] + s2[0] - s2[0] / 3 * 6 + 2) % 4;
+        if (m != 0) sol.push_back(static_cast<TurnMove>(s2[0] / 3 * 3 + m - 1));
+        sol.insert(sol.end(), s2.begin() + 1, s2.end());
+    } else {
+        sol.insert(sol.end(), s1.begin(), s1.end());
+        sol.insert(sol.end(), s2.begin(), s2.end());
+    }
+    return {SolveResult::Success, show::to_string(sol)};
+}
+bool is_solvable(std::string_view color_cube) {
+    return is_valid_config(color_cube) &&
+           ColorState::fromString(color_cube).toCubieCube().isSolvable();
+}
+std::string apply_maneuver(std::string_view maneuver, std::string_view cube_state) {
+    if (cube_state.size() != 54 || !is_valid_maneuver(maneuver)) {
+        throw std::invalid_argument("Invalid cube format or maneuver string.");
+    }
+    const auto ms = internal::parse_maneuver(maneuver);
+    return internal::apply_maneuver(cube_state, ms);
+}
+std::string show_permutation(std::string_view ms_or_cube, PermFormat format) {
+    std::string cube_str;
+    if (is_valid_config(ms_or_cube)) {
+        cube_str = ms_or_cube;
+    } else if (is_valid_maneuver(ms_or_cube)) {
+        auto ms = internal::parse_maneuver(ms_or_cube);
+        cube_str = internal::apply_maneuver(cubeId, ms);
+    } else {
+        throw std::invalid_argument("Input is neither a valid configuration nor a valid maneuver.");
+    }
+    auto cc = ColorState::fromString(cube_str).toCubieCube();
+    return show::to_string(cc, show::CubeFormat(int(format)));
+}
+}
+#ifdef __cplusplus
+    #define Default(x) = x
+#else
+    #define Default(x)
+#endif
+#define CUBE_BS 128
+#define CUBE_ID "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"
+#ifndef CF_ENUM
+    #if defined(__cplusplus)
+        #define CF_ENUM(_type, _name) \
+            int __CF_ENUM_##_name; \
+            enum _name : _type
+    #elif defined(__clang__) || defined(__OBJC__)
+        #define CF_ENUM(_type, _name) \
+            enum _name : _type _name; \
+            enum _name: _type
+    #else
+        #define CF_ENUM(_type, _name) \
+            _type _name; \
+            enum
+    #endif
+#endif
+typedef CF_ENUM(int32_t,SolveResult) {
+    SolveResultSuccess = 0,
+    SolveResultUnsolvable = 1,
+    SolveResultNotFound = 2,
+    SolveResultInvalidSrc = 3,
+    SolveResultInvalidTgt = 4,
+    SolveResultUnknownErr = 5
+};
+static inline const char* solve_result_info(SolveResult sr)
+{
+    switch(sr) {
+    case SolveResultSuccess:    return "Success.";
+    case SolveResultUnsolvable: return "The cube configuration is unsolvable.";
+    case SolveResultNotFound:   return "No solution found within the step limit.";
+    case SolveResultInvalidSrc: return "Invalid source color configuration.";
+    case SolveResultInvalidTgt: return "Invalid target color configuration.";
+    case SolveResultUnknownErr: return "Unknown error.";
+    default:                    return "???";
+    }
+}
+#ifdef __cplusplus
+extern "C" {
+#endif
+CUBE_EXPORT const char *solve_result_to_string(int32_t sr);
+CUBE_EXPORT void cube_init(
+    bool use_symmetry   Default(CUBE_USE_SYMMETRY),
+    const char* dir     Default(0)
+);
+CUBE_EXPORT SolveResult solve(
+    char*       buf,
+    const char* src         Default(CUBE_ID),
+    const char* tgt         Default(CUBE_ID),
+    int         good        Default(22)
+);
+CUBE_EXPORT bool solvable(
+    const char* color_cube  Default(CUBE_ID)
+);
+CUBE_EXPORT bool facecube(
+    char* buf,
+    const char* maneuver    Default(""),
+    const char *cube        Default(CUBE_ID)
+);
+CUBE_EXPORT bool permutation(
+    char*       buf,
+    const char* ms_or_cube  Default(CUBE_ID),
+    int         format      Default(2)
+);
+#ifdef __cplusplus
+}
+#endif
+extern "C" {
+void cube_init(bool use_symmetry, const char* dir)
+{
+    cube::cube_init(use_symmetry, dir ? std::string_view(dir) : std::string_view{});
+}
 const char* solve_result_to_string(int sr)
 {
     return solve_result_info(static_cast<SolveResult>(sr));
diff --git a/cube-hs.cabal b/cube-hs.cabal
--- a/cube-hs.cabal
+++ b/cube-hs.cabal
@@ -20,7 +20,7 @@
 -- PVP summary:     +-+------- breaking API changes
 --                  | | +----- non-breaking API additions
 --                  | | | +--- code changes with no API change
-version:            0.5.0.0
+version:            0.5.1.0
 
 -- A short (one-line) description of the package.
 synopsis:           High-performance, type-safe Rubik's Cube solver with C FFI bindings.
@@ -88,7 +88,7 @@
 
     cxx-sources:        cbits/cube_amalg.min.cpp
     include-dirs:       cbits
-    cxx-options:        -std=c++17 -O3
+    cxx-options:        -std=c++17 -O3 -DCUBE_USE_SYMMETRY=1 -DCUBE_ENABLE_TRACE=1
 
 test-suite cube-hs-test
     -- Import common warning flags.
diff --git a/src/Data/Cube.hs b/src/Data/Cube.hs
--- a/src/Data/Cube.hs
+++ b/src/Data/Cube.hs
@@ -27,7 +27,9 @@
 --     let scrambledCube = foldl (&>) cubeId scrambledTurns
 --     putStrLn $ "Scrambled state: " ++ showCube scrambledCube
 --
---     -- 4. Solve using zero-cost C FFI solver and verify restoration
+--     -- 4a. build tables (`False` means without symmetry) under "cache" directory
+--     cubeInit False "cache"  
+--     -- 4b. Solve the scrambled cube & verify solution
 --     let sol = unsafeSolveFrom scrambledCube
 --     putStrLn $ "Optimal solution steps: " ++ showTurns sol
 --     putStrLn $ "Is fully restored? " ++ show (applyTurns scrambledCube sol == cubeId)
@@ -51,6 +53,7 @@
 , showTurns
 , parseTurns
   -- * Solving Interface (C FFI)
+, cubeInit
 , rawSolve
 , solve
 , unsafeSolve
diff --git a/src/Data/Cube/FFI.hs b/src/Data/Cube/FFI.hs
--- a/src/Data/Cube/FFI.hs
+++ b/src/Data/Cube/FFI.hs
@@ -3,6 +3,12 @@
 import Foreign.C.Types (CInt(..), CBool(..))
 import Foreign.C.String (CString)
 
+foreign import ccall "cube_init"
+    c_cube_init 
+        :: CBool    -- use symmetry 
+        -> CString  -- table directory
+        -> IO ()
+
 foreign import ccall "solve"
     c_solve 
         :: CString  -- buffer
diff --git a/src/Data/Cube/Internal.hs b/src/Data/Cube/Internal.hs
--- a/src/Data/Cube/Internal.hs
+++ b/src/Data/Cube/Internal.hs
@@ -23,6 +23,7 @@
 , applyTurns
 , showTurns
 , parseTurns
+, cubeInit
 , rawSolve
 , solve
 , unsafeSolve
@@ -104,6 +105,24 @@
         Dx1 -> "D"; Dx2 -> "D2"; Dx3 -> "D'"
         Lx1 -> "L"; Lx2 -> "L2"; Lx3 -> "L'"
         Bx1 -> "B"; Bx2 -> "B2"; Bx3 -> "B'"
+
+-- | Initialize the cube library by loading or building lookup tables.
+--
+-- @useSym@: 'False' uses compact tables (<10 MB, fast build, slower
+-- solving); 'True' uses symmetry tables (~260 MB, slow build, fasters olving).
+--
+-- @dir@: directory to read\/write table files; empty means the default.
+-- Tables are loaded if present, otherwise built there.
+--
+-- Only the first call takes effect; later calls are no-ops. The first 
+-- 'c_solve' invokes it implicitly with defaults.
+--
+-- Warning: @useSym = True@ may take minutes and write ~260 MB on first run.
+cubeInit :: Bool -> FilePath -> IO ()
+cubeInit useSymmetry tableDir = do
+    let cb = if useSymmetry then 1 else 0
+    withCString tableDir $ \cstr -> do
+        c_cube_init cb cstr
 
 -- | Uses Kociemba's two-phase algorithm. The search stops as soon as any solution 
 -- of length at most `good` is found, trading optimality for speed. 
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -23,6 +23,8 @@
 
     assertTest "Standard initial cube is legal" 
                 (isLegalCubeString initStr)
+
+    cubeInit False "cache"
     
     let identityMoves = unsafeSolveFrom cubeId
     assertTest "Solving already solved cube returns no moves" 
