diff --git a/src/nanoemoji/color_glyph.py b/src/nanoemoji/color_glyph.py
index 96a72f72..4a042c48 100644
--- a/src/nanoemoji/color_glyph.py
+++ b/src/nanoemoji/color_glyph.py
@@ -49,7 +49,7 @@
 import pathops
 
 
-def _scale_viewbox_to_font_metrics(
+def scale_viewbox_to_font_metrics(
     view_box: Rect, ascender: int, descender: int, width: int
 ):
     assert descender <= 0
@@ -71,7 +71,7 @@ def map_viewbox_to_font_space(
 ) -> Affine2D:
     return Affine2D.compose_ltr(
         [
-            _scale_viewbox_to_font_metrics(view_box, ascender, descender, width),
+            scale_viewbox_to_font_metrics(view_box, ascender, descender, width),
             # flip y axis and shift so things are in the right place
             Affine2D(1, 0, 0, -1, 0, ascender),
             user_transform,
@@ -85,7 +85,7 @@ def map_viewbox_to_otsvg_space(
 ) -> Affine2D:
     return Affine2D.compose_ltr(
         [
-            _scale_viewbox_to_font_metrics(view_box, ascender, descender, width),
+            scale_viewbox_to_font_metrics(view_box, ascender, descender, width),
             # shift things in the [+x,-y] quadrant where OT-SVG expects them
             Affine2D(1, 0, 0, 1, 0, -ascender),
             user_transform,
diff --git a/src/nanoemoji/maximum_color.py b/src/nanoemoji/maximum_color.py
index 8f1cc89a..355ab1a7 100644
--- a/src/nanoemoji/maximum_color.py
+++ b/src/nanoemoji/maximum_color.py
@@ -65,6 +65,7 @@
 class WriteFontInputs(NamedTuple):
     glyphmap_file: Path
     config_file: Path
+    part_file: Path
 
     @property
     def table_tag(self) -> str:
@@ -88,7 +89,11 @@ def color_format(self) -> str:
     @classmethod
     def for_tag(cls, table_tag: str) -> "WriteFontInputs":
         basename = table_tag.strip()
-        return cls(Path(basename + ".glyphmap"), Path(basename + ".toml"))
+        return cls(
+            Path(basename + ".glyphmap"),
+            Path(basename + ".toml"),
+            master_part_file_dest(),
+        )
 
 
 def _vector_color_table(font: ttLib.TTFont) -> str:
@@ -121,6 +126,14 @@ def picosvg_dest(input_svg: Path) -> Path:
     return picosvg_dir() / input_svg.name
 
 
+def part_file_dest(picosvg_file: Path) -> Path:
+    return picosvg_file.with_suffix(".parts.json")
+
+
+def master_part_file_dest() -> Path:
+    return Path("parts-merged.json")
+
+
 def bitmap_dir() -> Path:
     return build_dir() / "bitmap"
 
@@ -163,7 +176,7 @@ def _write_preamble(nw: NinjaWriter):
     module_rule(
         nw,
         "write_font",
-        f"--glyphmap_file $glyphmap_file --config_file $config_file --output_file $out",
+        f"--config_file $config_file --glyphmap_file $glyphmap_file  --part_file $part_file --output_file $out",
     )
     nw.newline()
 
@@ -173,6 +186,22 @@ def _write_preamble(nw: NinjaWriter):
     )
     nw.newline()
 
+    module_rule(
+        nw,
+        "write_part_file",
+        f"--reuse_tolerance $reuse_tolerance --wh $wh --output_file $out $in",
+    )
+    nw.newline()
+
+    module_rule(
+        nw,
+        "write_combined_part_files",
+        f"--output_file $out  @$out.rsp",
+        rspfile="$out.rsp",
+        rspfile_content="$in",
+    )
+    nw.newline()
+
     # set height only, let width scale proportionally
     res = config.load().bitmap_resolution
     nw.rule(
@@ -237,6 +266,30 @@ def _picosvgs(nw: NinjaWriter, svg_files: List[Path]) -> List[Path]:
     return picosvgs
 
 
+def _part_file(
+    nw: NinjaWriter, font_config: config.FontConfig, picosvg_files: List[Path]
+) -> Path:
+    part_files = [part_file_dest(p) for p in picosvg_files]
+    for picosvg_file, part_file in zip(picosvg_files, part_files):
+        nw.build(
+            part_file,
+            "write_part_file",
+            picosvg_file,
+            variables={
+                "reuse_tolerance": font_config.reuse_tolerance,
+                "wh": font_config.ascender - font_config.descender,
+            },
+        )
+
+    nw.build(
+        master_part_file_dest(),
+        "write_combined_part_files",
+        sorted(part_files),
+    )
+
+    return master_part_file_dest()
+
+
 def _generate_additional_color_table(
     nw: NinjaWriter,
     input_font: Path,
@@ -283,7 +336,10 @@ def _generate_additional_color_table(
 
 
 def _generate_svg_from_colr(
-    nw: NinjaWriter, input_font: Path, font: ttLib.TTFont
+    nw: NinjaWriter,
+    font_config: config.FontConfig,
+    input_font: Path,
+    font: ttLib.TTFont,
 ) -> Tuple[Path, List[Path]]:
     # generate svgs
     svg_files = [
@@ -294,6 +350,8 @@ def _generate_svg_from_colr(
 
     # create and merge an SVG table
     picosvgs = _picosvgs(nw, svg_files)
+    part_file = _part_file(nw, font_config, picosvgs)
+
     output_file = _generate_additional_color_table(
         nw, input_font, picosvgs + [input_font], "SVG ", input_font
     )
@@ -301,7 +359,10 @@ def _generate_svg_from_colr(
 
 
 def _generate_colr_from_svg(
-    nw: NinjaWriter, input_font: Path, font: ttLib.TTFont
+    nw: NinjaWriter,
+    font_config: config.FontConfig,
+    input_font: Path,
+    font: ttLib.TTFont,
 ) -> Tuple[Path, List[Path]]:
     # extract the svgs
     svg_files = [
@@ -312,6 +373,8 @@ def _generate_colr_from_svg(
 
     # create and merge a COLR table
     picosvgs = _picosvgs(nw, svg_files)
+    part_file = _part_file(nw, font_config, picosvgs)
+
     output_file = _generate_additional_color_table(
         nw, input_font, picosvgs + [input_font], "COLR", input_font
     )
@@ -371,7 +434,8 @@ def _run(argv):
     input_file = Path(argv[1]).resolve()  # we need a non-relative path
     assert input_file.is_file()
     font = ttLib.TTFont(input_file)
-    final_output = Path(config.load().output_file)
+    font_config = config.load()
+    final_output = Path(font_config.output_file)
     assert (
         input_file.resolve() != (build_dir() / final_output).resolve()
     ), "In == Out is bad"
@@ -391,9 +455,13 @@ def _run(argv):
 
             # generate the missing vector table
             if color_table == "COLR":
-                wip_file, picosvg_files = _generate_svg_from_colr(nw, wip_file, font)
+                wip_file, picosvg_files = _generate_svg_from_colr(
+                    nw, font_config, wip_file, font
+                )
             else:
-                wip_file, picosvg_files = _generate_colr_from_svg(nw, wip_file, font)
+                wip_file, picosvg_files = _generate_colr_from_svg(
+                    nw, font_config, wip_file, font
+                )
 
             if FLAGS.bitmaps:
                 wip_file = _generate_cbdt(nw, input_file, font, wip_file, picosvg_files)
diff --git a/src/nanoemoji/nanoemoji.py b/src/nanoemoji/nanoemoji.py
index 41f7211e..55a7b9d9 100644
--- a/src/nanoemoji/nanoemoji.py
+++ b/src/nanoemoji/nanoemoji.py
@@ -206,10 +206,19 @@ def write_preamble(nw):
     )
     nw.newline()
 
+    module_rule(
+        nw,
+        "write_combined_part_files",
+        f"--output_file $out  @$out.rsp",
+        rspfile="$out.rsp",
+        rspfile_content="$in",
+    )
+    nw.newline()
+
     module_rule(
         nw,
         "write_font",
-        f"--config_file $config_file --fea_file $fea_file --glyphmap_file $glyphmap_file $in",
+        f"--config_file $config_file --fea_file $fea_file --glyphmap_file $glyphmap_file --part_file $part_file $in",
     )
 
     module_rule(
@@ -252,7 +261,7 @@ def write_preamble(nw):
     module_rule(
         nw,
         "write_part_file",
-        f"--reuse_tolerance $reuse_tolerance --output_file $out $in",
+        f"--reuse_tolerance $reuse_tolerance --wh $wh --output_file $out $in",
     )
     nw.newline()
 
@@ -344,31 +353,45 @@ def diff_png_dest(input_svg: Path) -> Path:
     return _dest_for_src(diff_png_dest, diff_bitmap_dir(), input_svg, ".png")
 
 
+def master_part_file_dest() -> Path:
+    return Path("parts-merged.json")
+
+
 def write_picosvg_builds(
     picosvg_builds: Set[Path],
     nw: NinjaWriter,
-    clipped: bool,
-    reuse_tolerance: float,
+    font_config: FontConfig,
     master: MasterConfig,
-):
+) -> Tuple[Set[Path], Set[Path]]:
     rule_name = "picosvg_unclipped"
-    if clipped:
+    if font_config.clip_to_viewbox:
         rule_name = "picosvg_clipped"
+
+    picosvgs = set()
+    part_files = set()
     for svg_file in master.sources:
         svg_file = abspath(svg_file)
-        dest = picosvg_dest(clipped, svg_file)
+        dest = picosvg_dest(font_config.clip_to_viewbox, svg_file)
         if svg_file in picosvg_builds:
             continue
         picosvg_builds.add(svg_file)
         nw.build(dest, rule_name, rel_build(svg_file))
 
+        part_dest = part_file_dest(dest)
         nw.build(
-            part_file_dest(dest),
+            part_dest,
             "write_part_file",
             dest,
-            variables={"reuse_tolerance": reuse_tolerance},
+            variables={
+                "reuse_tolerance": font_config.reuse_tolerance,
+                "wh": font_config.ascender - font_config.descender,
+            },
         )
 
+        picosvgs.add(dest)
+        part_files.add(part_dest)
+    return (picosvgs, part_files)
+
 
 def write_bitmap_builds(
     bitmap_builds: Set[Path],
@@ -525,6 +548,7 @@ def _variables_for_font_build(
         "config_file": rel_build(config_file),
         "fea_file": rel_build(_fea_file(font_config)),
         "glyphmap_file": rel_build(_glyphmap_file(font_config, master)),
+        "part_file": master_part_file_dest(),
     }
 
 
@@ -637,20 +661,28 @@ def _run(argv):
                 for master in font_config.masters:
                     write_glyphmap_build(nw, font_config, master)
 
-            picosvg_builds = set()
+            picosvg_builds = set()  # svgs for which we already made a picosvg
+            part_files = set()
             for font_config in font_configs:
                 for master in font_config.masters:
                     if font_config.has_picosvgs:
-                        write_picosvg_builds(
+                        _, parts = write_picosvg_builds(
                             picosvg_builds,
                             nw,
-                            font_config.clip_to_viewbox,
-                            font_config.reuse_tolerance,
+                            font_config,
                             master,
                         )
+                        part_files |= parts
             nw.newline()
 
-            bitmap_builds = set()
+            # Write a combined part file (potentially empty)
+            nw.build(
+                master_part_file_dest(),
+                "write_combined_part_files",
+                sorted(part_files),
+            )
+
+            bitmap_builds = set()  # svgs for which we already made a bitmap
             for font_config in font_configs:
                 if font_config.has_bitmaps:
                     assert not font_config.is_vf
@@ -663,8 +695,8 @@ def _run(argv):
                     )
             nw.newline()
 
-            zopflipng_builds = set()
-            pngquant_builds = set()
+            zopflipng_builds = set()  # svgs for which we already made a zopflipng
+            pngquant_builds = set()  # svgs for which we already made a pngquant
             for font_config in font_configs:
                 if not font_config.has_bitmaps or not (
                     font_config.use_zopflipng or font_config.use_pngquant
diff --git a/src/nanoemoji/parts.py b/src/nanoemoji/parts.py
index 12f15561..fd27c025 100644
--- a/src/nanoemoji/parts.py
+++ b/src/nanoemoji/parts.py
@@ -11,19 +11,44 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+"""A cache of reusable parts, esp paths, for whatever purpose you see fit.
+
+Intended to be used as a building block for glyph reuse.
+
+We always apply nop transforms to ensure any command type flips, such as arcs
+to cubics, occur. This ensures that if we merge a part file with no transform
+with one that has transformation the command types still align.
+"""
 
 import dataclasses
-from functools import lru_cache
+from functools import lru_cache, partial, reduce
 import json
 from nanoemoji.config import FontConfig
+from nanoemoji.color_glyph import scale_viewbox_to_font_metrics
 from pathlib import Path
+from picosvg.geometric_types import Rect
 from picosvg.svg import SVG
-from picosvg.svg_reuse import normalize
+from picosvg.svg_meta import cmd_coords
+from picosvg.svg_reuse import affine_between, normalize
+from picosvg.svg_transform import Affine2D
 from picosvg.svg_types import SVGPath, SVGShape
-from typing import Iterable, List, MutableMapping, NewType, Set, Tuple, Union
+from typing import (
+    Iterable,
+    List,
+    MutableMapping,
+    NamedTuple,
+    NewType,
+    Optional,
+    Set,
+    Tuple,
+    Union,
+)
+
 
+PathSource = Union[SVG, "ReusableParts"]
 
-PathSource = Union[SVGShape, Iterable[SVGShape], "ReuseableParts"]
+
+_DEFAULT_ROUND_NDIGITS = 3
 
 
 @lru_cache(maxsize=1)
@@ -56,78 +81,219 @@ def _is_iterable_of(thing, desired_type) -> bool:
 ShapeSet = NewType("ShapeSet", Set[Shape])
 
 
+class ReuseResult(NamedTuple):
+    transform: Affine2D
+    shape: Shape
+
+
+@lru_cache(maxsize=512)
+def _bbox_area(shape: Shape) -> float:
+    bbox = SVGPath(d=shape).bounding_box()
+    return bbox.w * bbox.h
+
+
+def _round(shape: SVGShape) -> SVGPath:
+    return shape.as_path().round_floats(_DEFAULT_ROUND_NDIGITS)
+
+
+def as_shape(path: SVGPath) -> Shape:
+    # apply a nop transform because some things still change, like arcs to cubics
+    path = path.apply_transform(Affine2D.identity())
+    return Shape(_round(path).d)
+
+
+# TODO: create a parts builder and a frozen parts from compute_donors() to more explicitly model the add/use cycle
+
+
 @dataclasses.dataclass
-class ReuseableParts:
+class ReusableParts:
     version: Tuple[int, int, int] = (1, 0, 0)
+    view_box: Rect = Rect(0, 0, 1, 1)
     reuse_tolerance: float = dataclasses.field(default_factory=_default_tolerence)
     shape_sets: MutableMapping[NormalizedShape, ShapeSet] = dataclasses.field(
         default_factory=dict
     )
+    _donor_cache: MutableMapping[NormalizedShape, Optional[Shape]] = dataclasses.field(
+        default_factory=dict
+    )
+
+    def normalize(self, path: str) -> NormalizedShape:
+        if self.reuse_tolerance != -1:
+            # normalize handles it's own rounding
+            # apply a nop transform because some things still change, like arcs to cubics
+            norm = NormalizedShape(
+                normalize(
+                    SVGPath(d=path).apply_transform(Affine2D.identity()),
+                    self.reuse_tolerance,
+                ).d
+            )
+        else:
+            norm = NormalizedShape(path)
+        return norm
 
     def _add_norm_path(self, norm: NormalizedShape, shape: Shape):
         if norm not in self.shape_sets:
             self.shape_sets[norm] = ShapeSet(set())
         self.shape_sets[norm].add(shape)
+        self._donor_cache.pop(norm, None)
 
     def _add(self, shape: Shape):
-        norm = NormalizedShape(shape)
-        if self.reuse_tolerance != -1:
-            norm = NormalizedShape(normalize(SVGPath(d=shape), self.reuse_tolerance).d)
+        norm = self.normalize(shape)
         self._add_norm_path(norm, shape)
 
     def add(self, source: PathSource):
-        if isinstance(source, ReuseableParts):
-            for normalized, shape_set in source.shape_sets.items():
-                for shape in shape_set:
-                    self._add_norm_path(normalized, shape)
+        """Combine two sets of parts. Source shapes will be scaled to dest viewbox."""
+        if isinstance(source, ReusableParts):
+            transform = Affine2D.rect_to_rect(source.view_box, self.view_box)
+            shapes = tuple(
+                reduce(lambda a, c: a | c, source.shape_sets.values(), set())
+            )
+        elif isinstance(source, SVG):
+            source.checkpicosvg()
+            source_box = source.view_box()
+            transform = scale_viewbox_to_font_metrics(
+                self.view_box, source_box.h, 0, source_box.w
+            )
+            shapes = tuple(s.as_path() for s in source.shapes())
         else:
-            if not _is_iterable_of(source, SVGShape):
-                source = (source,)
-            for a_source in source:
-                if not isinstance(a_source, SVGShape):
-                    raise ValueError(f"Illegal source {type(a_source)}")
-                svg_shape: SVGShape = a_source  # pytype: disable=attribute-error
-                self._add(Shape(svg_shape.as_path().d))
+            raise ValueError(f"Unknown part source: {type(source)}")
+
+        for shape in shapes:
+            if isinstance(shape, str):
+                shape = SVGPath(d=shape)
+            if transform != Affine2D.identity():
+                shape = shape.apply_transform(transform)
+            self._add(as_shape(shape))
+
+    def _compute_donor(self, norm: NormalizedShape):
+        self._donor_cache[norm] = None  # no solution
+
+        # try to select a donor that can fulfil every member of the set
+        # the input shape is in the set so if found we can get from donor => input
+        # shrinking a big thing is more likely to result in small #s that fit into
+        # more compact PaintTransform variants so try biggest first
+
+        # NOTE there are cases where this picks a suboptimal transform, e.g. a 2x3
+        # downscale be used when a scale uniform around center upscale might work
+        # Ex SVGPath(d="M8,13 A3 3 0 1 1 2,13 A3 3 0 1 1 8,13 Z")
+        #    SVGPath(d="M11,5 A2 2 0 1 1 7,5 A2 2 0 1 1 11,5 Z")
+        # A fancier implementation would factor in the # of occurences and the cost
+        # based on which shape is selected as donor if there are many possibilities.
+
+        svg_paths = sorted(
+            self.shape_sets[norm], key=lambda s: (_bbox_area(s), s), reverse=True
+        )
+        svg_paths = [SVGPath(d=s) for s in svg_paths]
+
+        for svg_path in svg_paths:
+            if all(
+                affine_between(svg_path, svg_path2, self.reuse_tolerance) is not None
+                for svg_path2 in svg_paths
+            ):
+                # Do NOT use as_shape; these paths already passed through it
+                self._donor_cache[norm] = Shape(svg_path.d)
+                break
+
+    def compute_donors(self):
+        self._donor_cache.clear()
+        for norm in self.shape_sets:
+            self._compute_donor(norm)
+
+    def is_reused(self, shape: SVGPath) -> bool:
+        shape = as_shape(shape)
+        norm = self.normalize(shape)
+        if norm not in self.shape_sets:
+            return False
+        if len(self.shape_sets[norm]) < 2:
+            return False
+        if norm not in self._donor_cache:
+            self._compute_donor(norm)
+        return shape == self._donor_cache[norm]  # this shape provides!
+
+    def try_reuse(self, shape: SVGPath) -> Optional[ReuseResult]:
+        """Returns the shape and transform to use to build the input shape."""
+        shape = as_shape(shape)
+        if self.reuse_tolerance == -1:
+            return ReuseResult(Affine2D.identity(), shape)
+
+        norm = self.normalize(shape)
+
+        # The whole point is to pre-add, doing it on the fly reduces reuse
+        if norm not in self.shape_sets:
+            print(self.to_json())
+            raise ValueError(
+                f"You MUST pre-add your shapes. No set matches normalization {norm} for {shape}."
+            )
+
+        if shape not in self.shape_sets[norm]:
+            print(self.to_json())
+            raise ValueError(f"You MUST pre-add your shapes. {shape} is new to us.")
+
+        if norm not in self._donor_cache:
+            assert (
+                shape in self.shape_sets[norm]
+            ), f"The input shape must be in the group"
+            self._compute_donor(norm)
+
+        donor = self._donor_cache[norm]
+        if donor is None:
+            # bail out, no solution!
+            return None
+
+        affine = affine_between(
+            SVGPath(d=donor), SVGPath(d=shape), self.reuse_tolerance
+        )
+        assert (
+            affine is not None
+        ), f"Should only get here with a solution. Epic fail on {donor}, {shape.d}"
+        return ReuseResult(affine, donor)
 
     def to_json(self):
         json_dict = {
             "version": ".".join(str(v) for v in self.version),
             "reuse_tolerance": self.reuse_tolerance,
+            "view_box": " ".join(str(int(v)) for v in self.view_box),
             "shape_sets": [
-                {"normalized": n, "shapes": list(s)} for n, s in self.shape_sets.items()
+                {
+                    "normalized": n,
+                    "shapes": list(s),
+                    "donor": self._donor_cache.get(n, ""),
+                }
+                for n, s in self.shape_sets.items()
             ],
         }
         return json.dumps(json_dict, indent=2)
 
     @classmethod
-    def fromstring(cls, string) -> "ReuseableParts":
-        first = string.strip()[0]
-        parts = cls()
-        if first == "<":
-            svg = SVG.fromstring(string).topicosvg()
-            for shape in svg.shapes():
-                parts.add(SVGPath(d=shape.as_path().d))
-        elif first == "{":
-            json_dict = json.loads(string)
-            parts.version = tuple(int(v) for v in json_dict.pop("version").split("."))
-            assert parts.version == (1, 0, 0), f"Bad version {parts.version}"
-            parts.reuse_tolerance = float(json_dict.pop("reuse_tolerance"))
-            for shape_set_json in json_dict.pop("shape_sets"):
-                norm = NormalizedShape(shape_set_json.pop("normalized"))
-                shapes = ShapeSet({Shape(s) for s in shape_set_json.pop("shapes")})
-                if shape_set_json:
-                    raise ValueError(f"Unconsumed input {shape_set_json}")
-                parts.shape_sets[norm] = shapes
-            if json_dict:
-                raise ValueError(f"Unconsumed input {json_dict}")
-
-        else:
-            raise ValueError(f"Unrecognized start sequence {string[:16]}")
+    def from_json(cls, string: str) -> "ReusableParts":
+        json_dict = json.loads(string)
+        parts = ReusableParts()
+        parts.version = tuple(int(v) for v in json_dict.pop("version").split("."))
+        assert parts.version == (1, 0, 0), f"Bad version {parts.version}"
+        parts.view_box = Rect(*(int(v) for v in json_dict.pop("view_box").split(" ")))
+        assert parts.view_box[:2] == (
+            0,
+            0,
+        ), f"Must be a viewbox from 0,0 {parts.view_box}"
+        parts.reuse_tolerance = float(json_dict.pop("reuse_tolerance"))
+        for shape_set_json in json_dict.pop("shape_sets"):
+            norm = NormalizedShape(shape_set_json.pop("normalized"))
+            shapes = ShapeSet({Shape(s) for s in shape_set_json.pop("shapes")})
+            donor = shape_set_json.pop("donor")
+            if donor and donor not in shapes:
+                raise ValueError("Donor must be in group")
+            if shape_set_json:
+                raise ValueError(f"Unconsumed input {shape_set_json}")
+            parts.shape_sets[norm] = shapes
+            if donor != "":
+                parts._donor_cache[norm] = donor
+        if json_dict:
+            raise ValueError(f"Unconsumed input {json_dict}")
         return parts
 
     @classmethod
-    def load(cls, input_file: Path) -> "ReuseableParts":
+    def loadjson(cls, input_file: Path) -> "ReusableParts":
         ext = input_file.suffix.lower()
-        if ext not in {".svg", ".json"}:
+        if ext != ".json":
             raise ValueError(f"Unknown format {input_file}")
-        return cls.fromstring(input_file.read_text(encoding="utf-8"))
+        return cls.from_json(input_file.read_text(encoding="utf-8"))
diff --git a/src/nanoemoji/write_combined_part_files.py b/src/nanoemoji/write_combined_part_files.py
new file mode 100644
index 00000000..8645f3f6
--- /dev/null
+++ b/src/nanoemoji/write_combined_part_files.py
@@ -0,0 +1,50 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Combines N part files to 1"""
+
+
+from absl import app
+from absl import flags
+from nanoemoji.parts import ReusableParts
+from nanoemoji import util
+from pathlib import Path
+
+
+FLAGS = flags.FLAGS
+
+
+def main(argv):
+    input_files = util.expand_ninja_response_files(argv[1:])
+
+    combined_parts = ReusableParts()
+    individual_parts = [ReusableParts.loadjson(Path(p)) for p in input_files]
+    if individual_parts:
+        combined_parts.version = util.only({p.version for p in individual_parts})
+        combined_parts.reuse_tolerance = util.only(
+            {p.reuse_tolerance for p in individual_parts}
+        )
+        combined_parts.view_box = util.only({p.view_box for p in individual_parts})
+
+    for parts in individual_parts:
+        combined_parts.add(parts)
+
+    combined_parts.compute_donors()  # precompute for later use
+
+    with util.file_printer(FLAGS.output_file) as print:
+        print(combined_parts.to_json())
+
+
+if __name__ == "__main__":
+    app.run(main)
diff --git a/src/nanoemoji/write_font.py b/src/nanoemoji/write_font.py
index 21711e6d..919735d5 100644
--- a/src/nanoemoji/write_font.py
+++ b/src/nanoemoji/write_font.py
@@ -51,6 +51,7 @@
     PaintGlyph,
     PaintSolid,
 )
+from nanoemoji.parts import ReusableParts
 from nanoemoji.png import PNG
 from nanoemoji.svg import make_svg_table
 from nanoemoji.svg_path import draw_svg_path
@@ -86,6 +87,7 @@
 
 flags.DEFINE_string("config_file", None, "Config filename.")
 flags.DEFINE_string("glyphmap_file", None, "Glyphmap filename.")
+flags.DEFINE_string("part_file", None, "Reusable parts filename.")
 
 
 # A GlyphMapping plus an SVG, typically a picosvg, and/or a PNG
@@ -824,6 +826,10 @@ def main(argv):
 
     inputs = list(_inputs(font_config, glyphmap.parse_csv(FLAGS.glyphmap_file)))
 
+    reusable_parts = ReusableParts()
+    if FLAGS.part_file:
+        reusable_parts = ReusableParts.loadjson(Path(FLAGS.part_file))
+
     if not inputs:
         sys.exit("Please provide at least one svg filename")
     ufo, ttfont = _generate_color_font(font_config, inputs)
diff --git a/src/nanoemoji/write_part_file.py b/src/nanoemoji/write_part_file.py
index c1cd9f86..ef1f9227 100644
--- a/src/nanoemoji/write_part_file.py
+++ b/src/nanoemoji/write_part_file.py
@@ -12,37 +12,44 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Generates a part file from 1..N input part sources.
-
-Part sources can be:
-
-1. Other part files
-2. Svg files
-
-Or any mix thereof.
+"""Generates a part file from 1 source, typically an svg file.
 """
 
 
 from absl import app
 from absl import flags
-from functools import reduce
-from nanoemoji.parts import ReuseableParts
+from nanoemoji.parts import ReusableParts
 from nanoemoji import util
 from pathlib import Path
+from picosvg.geometric_types import Rect
+from picosvg.svg import SVG
 
 
 FLAGS = flags.FLAGS
 
 
+flags.DEFINE_integer("wh", None, "The width and height to use.")
+flags.DEFINE_bool("compute_donors", False, "Whether to compute donors.")
+
+
 def main(argv):
-    parts = [ReuseableParts.load(Path(part_file)) for part_file in argv[1:]]
-    if not parts:
-        raise ValueError("Specify at least one input")
-    parts = reduce(lambda a, c: a.add(c), parts[1:], parts[0])
+    if len(argv) != 2:
+        raise ValueError("Specify exactly one input")
+
+    view_box = Rect(0, 0, FLAGS.wh, FLAGS.wh)
+    parts = ReusableParts(view_box=view_box, reuse_tolerance=FLAGS.reuse_tolerance)
+
+    svg = SVG.parse(Path(argv[1]))
+    parts.add(svg)
+
+    if FLAGS.compute_donors:
+        parts.compute_donors()
 
     with util.file_printer(FLAGS.output_file) as print:
         print(parts.to_json())
 
 
 if __name__ == "__main__":
+    flags.mark_flag_as_required("wh")
+    flags.mark_flag_as_required("reuse_tolerance")
     app.run(main)
diff --git a/tests/circle.svg b/tests/circle.svg
new file mode 100644
index 00000000..1c39dab6
--- /dev/null
+++ b/tests/circle.svg
@@ -0,0 +1,3 @@
+<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="5" cy="5" r="2" />
+</svg>
diff --git a/tests/circle_10x.svg b/tests/circle_10x.svg
new file mode 100644
index 00000000..11c9c7b9
--- /dev/null
+++ b/tests/circle_10x.svg
@@ -0,0 +1,3 @@
+<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="50" cy="50" r="20" />
+</svg>
diff --git a/tests/conftest.py b/tests/conftest.py
index 5ef6e282..48ca1a82 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -45,6 +45,6 @@ def _cleanup_temporary_dirs(request):
         cleanup_temp_dirs()
     else:
         print(
-            f"NOT cleaning up {len(active_temp_dirs())} temp dirs to ease troubleshooting"
+            f"NOT cleaning up {len(active_temp_dirs())} temp dirs ({active_temp_dirs()}) to ease troubleshooting"
         )
     forget_temp_dirs()
diff --git a/tests/maximum_color_test.py b/tests/maximum_color_test.py
index 51e42560..ee0349fc 100644
--- a/tests/maximum_color_test.py
+++ b/tests/maximum_color_test.py
@@ -24,16 +24,6 @@
 from typing import Tuple
 
 
-@pytest.fixture(scope="module", autouse=True)
-def _cleanup_temporary_dirs():
-    # The mkdtemp() docs say the user is responsible for deleting the directory
-    # and its contents when done with it. So we use an autouse fixture that
-    # automatically removes all the temp dirs at the end of the test module
-    yield
-    # teardown happens after the 'yield'
-    cleanup_temp_dirs()
-
-
 def _build_initial_font(color_format: str) -> Path:
     tmp_dir = run_nanoemoji(
         (
diff --git a/tests/nanoemoji_test.py b/tests/nanoemoji_test.py
index 3ee9de43..8ae1e81d 100644
--- a/tests/nanoemoji_test.py
+++ b/tests/nanoemoji_test.py
@@ -68,9 +68,10 @@ def test_build_static_font_default_config_cli_svg_list():
 
 def _build_and_check_ttx(config_overrides, svgs, expected_ttx):
     config_file = mkdtemp() / "config.toml"
-    font_config, _ = color_font_config(
+    font_config, glyph_inputs = color_font_config(
         config_overrides, svgs, tmp_dir=config_file.parent
     )
+    del glyph_inputs
     config.write(config_file, font_config)
     print(config_file, font_config)
 
diff --git a/tests/parts_test.py b/tests/parts_test.py
index fa379390..74833b0c 100644
--- a/tests/parts_test.py
+++ b/tests/parts_test.py
@@ -12,10 +12,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from nanoemoji.parts import ReuseableParts
-from picosvg.svg_types import SVGCircle, SVGRect
+from nanoemoji.parts import ReusableParts
+from nanoemoji.util import only
+from picosvg.geometric_types import Rect
+from picosvg.svg import SVG
+from picosvg import svg_meta
+from picosvg.svg_types import SVGCircle, SVGPath, SVGRect
+from picosvg.svg_reuse import affine_between
+from pathlib import Path
 import pprint
 import pytest
+import re
 from test_helper import cleanup_temp_dirs, locate_test_file, mkdtemp
 
 
@@ -33,43 +40,75 @@ def _cleanup_temporary_dirs():
 # TODO we get pointless precision, e.g. 1.2000000000000002
 
 
-def check_num_shapes(parts: ReuseableParts, expected_shape_sets: int):
-    assert len(parts.shape_sets) == expected_shape_sets, ",".join(
-        sorted(str(p) for p in parts.shape_sets.keys())
-    )
+def _svg_commands(path: str) -> str:
+    print(path)
+    svg_cmds = "".join(svg_meta.cmds())
+    return re.sub(f"[^{svg_cmds}]+", "", path)
 
 
-def test_collects_normalized_shapes():
-    shapes = (
-        SVGRect(width=2, height=1),
-        SVGRect(width=4, height=2),
-        SVGCircle(r=2),
+def check_num_shapes(parts: ReusableParts, expected_shape_sets: int):
+    assert len(parts.shape_sets) == expected_shape_sets, ",".join(
+        sorted(str(p) for p in parts.shape_sets.keys())
     )
 
-    parts = ReuseableParts()
-    parts.add(shapes)
 
-    check_num_shapes(parts, 2)
+def _from_svg(svg, view_box=None) -> ReusableParts:
+    if isinstance(svg, str):
+        svg = SVG.fromstring(svg)
+    elif isinstance(svg, Path):
+        svg = SVG.parse(svg)
+    if view_box is None:
+        view_box = svg.view_box()
+    parts = ReusableParts(view_box=view_box)
+    parts.add(svg)
+    return parts
 
 
-def test_from_svg():
-    parts = ReuseableParts.load(locate_test_file("rect.svg"))
+def test_add_svg():
+    parts = _from_svg(
+        """
+        <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"
+             xmlns:xlink="http://www.w3.org/1999/xlink">
+          <rect x="2" y="2" width="6" height="2" fill="blue" />
+          <rect x="4" y="4" width="6" height="2" fill="blue" opacity="0.8" />
+        </svg>
+        """
+    )
     check_num_shapes(parts, 1)
 
 
-def test_merge():
-    shapes1 = (SVGRect(width=2, height=1),)
-    shapes2 = (
-        SVGRect(width=4, height=2),
-        SVGCircle(r=2),
+def test_collects_normalized_shapes():
+    parts = _from_svg(
+        """
+        <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
+          <rect width="2" height="1"/>
+          <rect width="4" height="2" y="1.5"/>
+          <circle cx="5" cy="5" r="2"/>
+        </svg>
+        """
     )
 
-    p1 = ReuseableParts()
-    p1.add(shapes1)
+    check_num_shapes(parts, 2)
+
+
+def test_simple_merge():
+    p1 = _from_svg(
+        """
+        <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
+          <rect width="2" height="1"/>
+        </svg>
+        """
+    )
     check_num_shapes(p1, 1)
 
-    p2 = ReuseableParts()
-    p2.add(shapes2)
+    p2 = _from_svg(
+        """
+        <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
+          <rect width="4" height="2" y="1.5"/>
+          <circle r="2"/>
+        </svg>
+        """
+    )
     check_num_shapes(p2, 2)
 
     p1.add(p2)
@@ -77,12 +116,141 @@ def test_merge():
 
 
 def test_file_io():
-    parts = ReuseableParts()
-    parts.add(ReuseableParts.load(locate_test_file("rect.svg")))
+    parts = _from_svg(locate_test_file("rect.svg"))
     check_num_shapes(parts, 1)
 
     tmp_dir = mkdtemp()
     tmp_file = tmp_dir / "rect.json"
     tmp_file.write_text(parts.to_json())
 
-    assert parts == ReuseableParts.load(tmp_file)
+    assert parts == ReusableParts.loadjson(tmp_file), parts.to_json()
+
+
+# Note that this is not meant to be the primary test of reuse, that's in
+# picosvg. This just checks we use those capabilities in the expected manner.
+@pytest.mark.parametrize(
+    "svg",
+    [
+        SVG.fromstring(
+            """
+            <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
+              <rect width="2" height="1"/>
+              <rect width="4" height="2" y="1.5"/>
+            </svg>
+            """
+        ).topicosvg(),
+        # https://github.com/googlefonts/nanoemoji/issues/415 arc normalization
+        SVG.fromstring(
+            """
+            <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
+              <circle r="1"/>
+              <circle r="2"/>
+            </svg>
+            """
+        ).topicosvg(),
+    ],
+)
+def test_reuse_finds_single_donor(svg):
+    parts = _from_svg(svg.tostring())
+
+    # There should be one shape used to create all the others
+    maybe_reuses = [parts.try_reuse(s.as_path()) for s in svg.shapes()]
+    assert all(ri is not None for ri in maybe_reuses), "All shapes should have results"
+    scale_up = {
+        ri for ri in maybe_reuses if not all(v <= 1.0 for v in ri.transform.getscale())
+    }
+    assert not scale_up, f"Should prefer to scale big to little {scale_up}"
+    assert (
+        len({ri.shape for ri in maybe_reuses}) == 1
+    ), f"{maybe_reuses} should all reuse the same shape"
+
+
+# Feed in two identical svgs, just one of them multiplies viewbox and coords by 10
+def test_reuse_with_inconsistent_square_viewbox():
+    little = locate_test_file("rect.svg")
+    big = locate_test_file("rect_10x.svg")
+
+    r1 = _from_svg(little)
+    assert r1.view_box == Rect(0, 0, 10, 10)
+    r1.add(_from_svg(big))
+    r1.compute_donors()
+
+    r2 = _from_svg(big)
+    assert r2.view_box == Rect(0, 0, 100, 100)
+    r2.add(_from_svg(little))
+    r2.compute_donors()
+
+    check_num_shapes(r1, 1)
+    check_num_shapes(r2, 1)
+    assert only(r1.shape_sets.values()) == {
+        "M2,2 L8,2 L8,4 L2,4 L2,2 Z",
+        "M4,4 L10,4 L10,6 L4,6 L4,4 Z",
+    }, "There should be 2 (not 4) shapes after scaled merge. r1 should use the little viewbox."
+    assert only(r2.shape_sets.values()) == {
+        "M20,20 L80,20 L80,40 L20,40 L20,20 Z",
+        "M40,40 L100,40 L100,60 L40,60 L40,40 Z",
+    }, "There should be 2 (not 4) shapes after scaled merge. r2 should use the big viewbox."
+
+
+def test_arcs_become_cubics():
+    parts = _from_svg(
+        """
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
+          <defs/>
+          <path d="M2,0 A2 2 0 1 1 -2,0 A2 2 0 1 1 2,0 Z"/>
+        </svg>
+        """
+    )
+
+    norm, path = only(parts.shape_sets.items())
+    path = only(path)
+    assert (_svg_commands(norm), _svg_commands(path)) == (
+        "Mccccz",
+        "MCCCCZ",
+    ), f"Wrong command types\nnorm {norm}\npath {path}"
+
+
+# scaling turns arcs into cubics
+# we need them to reuse regardless
+def test_scaled_merge_arcs_to_cubics():
+    parts = _from_svg(locate_test_file("circle_10x.svg"))
+    part2 = _from_svg(locate_test_file("circle.svg"))
+    assert parts.view_box == Rect(0, 0, 100, 100)
+    assert part2.view_box == Rect(0, 0, 10, 10)
+    parts.add(part2)
+
+    assert len(parts.shape_sets) == 1, parts.to_json()
+    norm, paths = only(parts.shape_sets.items())
+    path_cmds = tuple(_svg_commands(p) for p in paths)
+    assert (_svg_commands(norm),) + path_cmds == (
+        "Mccccz",
+        "MCCCCZ",
+        "MCCCCZ",
+    ), f"Path damaged\nnorm {norm}\npaths {paths}"
+
+
+def _start_at_origin(path):
+    cmd, args = next(iter(path))
+    assert cmd == "M"
+    x, y = args
+    return path.move(-x, -y)
+
+
+# SVGs with varied width that contains squares should push squares
+# into the part store, not get mangled into rectangles.
+def test_squares_stay_squares():
+    parts = ReusableParts(view_box=Rect(0, 0, 10, 10))
+
+    parts.add(SVG.parse(locate_test_file("square_vbox_narrow.svg")))
+    parts.add(SVG.parse(locate_test_file("square_vbox_square.svg")))
+    parts.add(SVG.parse(locate_test_file("square_vbox_narrow.svg")))
+
+    # Every square should have normalized the same
+    assert len(parts.shape_sets) == 1, parts.to_json()
+
+    paths = only(parts.shape_sets.values())
+
+    paths = [_start_at_origin(SVGPath(d=p)).relative(inplace=True) for p in paths]
+    assert {p.d for p in paths} == {
+        "M0,0 l3,0 l0,3 l-3,0 l0,-3 z"
+    }, "The square should remain 3x3; converted to relative and starting at 0,0 they should be identical"
diff --git a/tests/rect_10.svg b/tests/rect_10.svg
new file mode 100644
index 00000000..120ca76a
--- /dev/null
+++ b/tests/rect_10.svg
@@ -0,0 +1,3 @@
+<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
+  <rect x="4.5" y="1" width="1" height="8" />
+</svg>
diff --git a/tests/rect_1000.svg b/tests/rect_1000.svg
new file mode 100644
index 00000000..116cf050
--- /dev/null
+++ b/tests/rect_1000.svg
@@ -0,0 +1,4 @@
+<svg viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink">
+  <rect x="0" y="0" width="1000" height="1000" />
+</svg>
diff --git a/tests/rect_10x.svg b/tests/rect_10x.svg
new file mode 100644
index 00000000..4fa827a6
--- /dev/null
+++ b/tests/rect_10x.svg
@@ -0,0 +1,5 @@
+<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink">
+  <rect x="20" y="20" width="60" height="20" fill="blue" />
+  <rect x="40" y="40" width="60" height="20" fill="blue" opacity="0.8" />
+</svg>
diff --git a/tests/rect_from_colr_1.svg b/tests/rect_from_colr_1.svg
index 7596a90e..51acd717 100644
--- a/tests/rect_from_colr_1.svg
+++ b/tests/rect_from_colr_1.svg
@@ -2,4 +2,4 @@
   <defs/>
   <path fill="blue" d="M2,2 L2,4 L8,4 L8,2 Z"/>
   <path fill="blue" opacity="0.8" transform="translate(2, 2)" d="M2,2 L2,4 L8,4 L8,2 Z"/>
-</svg>
+</svg>
\ No newline at end of file