Path to adopt 32-bit implementations for `{KD, Ball}Tree`

## Motivation
Having and using 32-bit implementations of `{KD, Ball}Tree` allows for better preservation of dtype, lower memory footprint, and more consistent Cython code.

## Strategy
Work has already been started in #25914 to add the code for the new 32-bit implementations. These will not be directly used yet, and the PR instead focuses on the actual creation of the new classes. Consequently, `{KD, Ball}Tree` are bound to `{KD, Ball}Tree64` for consistency and backwards compatibility.

Following this PR, we will need to begin an API deprecation to move users away from constructing trees directly, and instead using a factory method (similar to `DistanceMetric.get_metric`). Then, later, we can separate `{KD, Ball}Tree` from `{KD, Ball}Tree64` so that we have a singular dispatcher `{KD, Ball}Tree` and two type-specialized implementations. 

The deprecation process involves:
1. Introducing `get_tree(...)` to `{KD, Ball}Tree64` with the same signature of `BinaryTree.__init__`, which is in charge of constructing the type-specialized trees directly.
2. Adding a `FutureWarning` to `BinaryTree.__init__` to begin deprecation, and suppressing the warning using a context manager in `{KD, Ball}Tree64.get_tree` to enforce it as the "correct" way to construct the trees.
3. **Complete the deprecation** by introducing a separate `{KD, Ball}Tree` into the hierarchy which comes with the same `get_tree` method and fully remove the `FutureWarning` from `{KD, Ball}Tree{32, 64}.__init__` since they should no longer be directly constructed, as well as getting rid of the `{KD, Ball}Tree{32, 64}.get_tree` method since it should only exist in `{KD, Ball}Tree`.

At the end, the expected API is:
```python
from sklearn.neighbors import KDTree

# Data
X = ...

# tree is thus an instance of KDTree{32, 64} based on X.dtype
tree = KDTree.get_tree(X, ...)
```

cc: @thomasjpfan @jjerphan @OmarManzoor

COMMENT:
I still prefer to avoid the extra indirection of having a `_tree` attribute if possible.

As an alternative, what about defining `{KD, Ball}Tree.__new__()` so that it directly produces the type-specialized implementation? This may result in changed models though since it means that `tree=KDTree(X_32)` now produces a tree where `tree.data` is of type `float32_t` rather than `float64_t`. Each solution we consider would have that downside as well, the only difference is that since this one has a backwards compatible API, it would actually be a model change rather than behavior change over two releases.

e.g.

```python
import numpy as np

X_64 = np.random.rand(20, 20).astype(np.float64)
X_32 = X_64.astype(np.float32)

class Tree:
    def __new__(cls, data):
        if data.dtype==np.float32:
            return object.__new__(Tree32)
        elif data.dtype==np.float64:
            return object.__new__(Tree64)

class Tree32(Tree):
    def __init__(self, data) -> None:
        print("Tree32 is initialized!")
        assert data.dtype == np.float32

class Tree64(Tree):
    def __init__(self, data) -> None:
        print("Tree64 is initialized!")
        assert data.dtype == np.float64

tree64 = Tree(X_64)
# Tree64 is initialized!

tree32 = Tree(X_32)
# Tree32 is initialized!

assert isinstance(tree64, Tree64)
assert isinstance(tree32, Tree32)
```

Labels: API, Needs Decision, RFC