```python
def parse_semver(version: str) -> tuple[int, int, int, str | None]:
    """
    Parses a semantic version string into its components.

    Args:
        version (str): The semantic version string to parse.

    Returns:
        tuple[int, int, int, str | None]: A tuple containing the major,
                                         minor, patch versions and the
                                         prerelease or build metadata as strings.
                                         If there is no prerelease or build
                                         metadata, it returns None for that part.

    Raises:
        ValueError: If the version string does not conform to semantic versioning
                   rules.
    """
    parts = version.split('-')
    if len(parts) > 2:
        raise ValueError("Too many parts in the version string.")

    major, minor, patch = parts[0].split('.')
    if not all(part.isdigit() for part in (major, minor, patch)):
        raise ValueError("Invalid numeric part in the version string.")

    major, minor, patch = int(major), int(minor), int(patch)

    prerelease = None
    build_metadata = None

    if len(parts) == 2:
        prerelease = parts[1]
        if not prerelease or any(char not in '0-9A-Za-z.-' for char in prerelease):
            raise ValueError("Invalid prerelease string.")

    return (major, minor, patch, prerelease)
```